swiftcrashswiftdataswift-testing

SwiftData and SwiftTesting: ModelContainer created in init() crashes the test


Recently I started playing with SwiftData and Testing framework.

I stumbled across an interesting crash. It occurs when inserting the model into ModelContext. However it happens only if I create my ModelContainer in the initializer. When it's created directly inside the test method, everything works fine.

Maybe someone could help me understand why it is so.

Thanks!


Here's the minimum reproducible example:

import Testing
import SwiftData

@MainActor @Suite
class StoreTests {
    var context: ModelContext!
    var store: Store!

    init() {
        let modelContainer = try! ModelContainer(
            for: Model.self,
            configurations: .init(isStoredInMemoryOnly: true)
        )
        context = modelContainer.mainContext
        store = Store(context: context)
    }

    @Test
    func testThatStoresModel_Init() throws {
        let model = Model(id: 0)
        try store.save(model) // Crashes with Thread 1: EXC_BREAKPOINT

        // (...)
    }

    @Test
    func testThatStoresModel_Inline() throws {
        let modelContainer = try! ModelContainer(
            for: Model.self,
            configurations: .init(isStoredInMemoryOnly: true)
        )
        let context = modelContainer.mainContext
        let store = Store(context: context)
        let model = Model(id: 0)
        try store.save(model) // Passes test

        // (...)
    }
}

@Model
class Model {    
    var id: Int

    init(id: Int) {
        self.id = id
    }
}

class Store {
    let context: ModelContext

    init(context: ModelContext) {
        self.context = context
    }

    func save(_ model: Model) throws {
        context.insert(model)
        try context.save()
    }
}

Solution

  • The root cause is basically the same as this question. You need to keep a strong reference to the model container, or else it will get deinitialised.

    The ModelContainer you created in init gets deinitialised after init returns, and you end up with a "hanging" ModelContext that has no container to insert into, when the test function is invoked.

    If you create the ModelContainer in the test function, it will live until the test function returns.

    If you keep a reference to the model container in the suite class,

    let container: ModelContainer // keep the model container alive!
    let context: ModelContext
    let store: Store
    
    init() {
        container = try! ModelContainer(
            for: Model.self,
            configurations: .init(isStoredInMemoryOnly: true)
        )
        context = container.mainContext
        store = Store(context: context)
    }
    

    Then testThatStoresModel_Init will pass as expected.