SwiftData ModelContext.insert crashes, why?

This simple test fails in my project. Similar code in my application also crashes.

  1. How do I debug the problem?
  2. What project settings are required. I have added SwiftData as a framework to test (and application) targets?

Thanks,

The problem is with: modelContext.insert(item)

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

import XCTest
import SwiftData

@Model
class FakeModel {
    var name: String
    init(name: String) { self.name = name }
}

@MainActor
final class FakeModelTests: XCTestCase {
    var modelContext: ModelContext!

    override func setUp() {
        super.setUp()
        do {
            let container = try ModelContainer(for: FakeModel.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
            modelContext = container.mainContext
        } catch {
            XCTFail("Failed to create ModelContainer: \(error)")
            modelContext = nil
        }
    }

    func testSaveFetchDeleteFakeItem() {
        guard let modelContext = modelContext else {
            XCTFail("ModelContext must be initialized")
            return
        }
        
        let item = FakeModel(name: "Test")
       modelContext.insert(item)

        let fetchDescriptor = FetchDescriptor<FakeModel>()
        let items = try! modelContext.fetch(fetchDescriptor)
        XCTAssertEqual(items.count, 1)
        XCTAssertEqual(items.first?.name, "Test")

        modelContext.delete(item)
        let itemsAfterDelete = try! modelContext.fetch(fetchDescriptor)
        XCTAssertEqual(itemsAfterDelete.count, 0)
    }
}
Answered by DTS Engineer in 855372022

Yeah, as @joadan said, when you grab the model context with container.mainContext, the context doesn't hold the model container, and so the container becomes invalid outside of the scope of the setUp method; when you create a context with ModelContext(container), the context holds the container, and so the container has the same lifecycle as the context. You can see this subtle difference by observing the value of the ModelContext._container (private) property in the Xcode debugger.

If you'd test your code with the main context, which is main-actor isolated, consider holding the model container instead:

@MainActor
final class FakeModelTests: XCTestCase {
    var modelContainer: ModelContainer!
 
    override func setUp() {
        super.setUp()
        do {
            modelContainer = try ModelContainer(for: FakeModel.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
        } catch {
            XCTFail("Failed to create ModelContainer: \(error)")
            modelContainer = nil
        }
    }
 
    func testSaveFetchDeleteFakeItem() {
        guard let modelContext = modelContainer?.mainContext else {
            XCTFail("ModelContext must be initialized")
            return
        }
        
        let item = FakeModel(name: "Test")
        modelContext.insert(item)
        ...
    }
}

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Answering my own question. Found this blog, searching for answers https://blog.jacobstechtavern.com/p/swiftdata-outside-swiftui Thanks Jacob.

The answer is simple, but maybe someone can explain. modelContext = container.mainContext works if it's modelContext = ModelContext(container)

Done!

Accepted Answer

I would keep the ModelContainer as a stored property and create the ModelContext in each test or even better create both the container and context objects in each test.

As for the issue with your initial code, the modelContext stored property is a reference to the modelContainer.mainContect but since the modelContainer object is local to the setUp method it will be released and the reference will no longer be valid when that method exits

in your second solution the modelContext is a newly created object so it will remain in memory as long as the test is running.

Yeah, as @joadan said, when you grab the model context with container.mainContext, the context doesn't hold the model container, and so the container becomes invalid outside of the scope of the setUp method; when you create a context with ModelContext(container), the context holds the container, and so the container has the same lifecycle as the context. You can see this subtle difference by observing the value of the ModelContext._container (private) property in the Xcode debugger.

If you'd test your code with the main context, which is main-actor isolated, consider holding the model container instead:

@MainActor
final class FakeModelTests: XCTestCase {
    var modelContainer: ModelContainer!
 
    override func setUp() {
        super.setUp()
        do {
            modelContainer = try ModelContainer(for: FakeModel.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
        } catch {
            XCTFail("Failed to create ModelContainer: \(error)")
            modelContainer = nil
        }
    }
 
    func testSaveFetchDeleteFakeItem() {
        guard let modelContext = modelContainer?.mainContext else {
            XCTFail("ModelContext must be initialized")
            return
        }
        
        let item = FakeModel(name: "Test")
        modelContext.insert(item)
        ...
    }
}

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Thanks for the explanation guys. The reason for the test was that I had similar code in my application. I create the ModelContainer in my App and set it on my DataManager, but it would be nil when trying to use it. It was tricky to find the issue.

SwiftData ModelContext.insert crashes, why?
 
 
Q