Mixing serialized and concurrent suites in Swift Testing

Hi,

I’m trying to express the following structure in Swift Testing:

@Suite(.serialized)
struct Root {
    @Suite(.serialized)
    struct SuiteA { /* ... */ }

    @Suite(.concurrent)
    struct SuiteB { /* ... */ }
}

Goal

  • SuiteA and SuiteB should be serialized (and not overlap with each other)
  • SuiteB should be fully concurrent internally, but also must not run at the same time as A

Problem

  • .concurrent doesn’t exist
  • Without it, SuiteB inherits .serialized from the parent
  • If I remove .serialized from the parent, then SuiteA and SuiteB can run in parallel with each other

So I can’t seem to express:

serialized between some suites, but concurrent within another


Question

Is there a way to model this today?

Thanks!

1 Like

A suite nested in a serialized suite cannot be run concurrently. This is by design, although it is not a technical constraint. It would help to know more about the real-world scenario you are testing that requires this configuration. :slightly_smiling_face:

Of interest to you (although I don't think it solves your specific problem) may be the pitched data-dependent serialization feature.

@grynspan Thank you for your reply.

My use case is SwiftData migration tests that need a file-backed SQLite store, living alongside a large E2E test suite that uses in-memory stores.

Migration tests must use file-backed storage because SwiftData migrations require closing and reopening the ModelContainer — you can't migrate an in-memory database since it's destroyed on close:

  @Suite(.serialized) class MigrationTests {
    var url: URL!
    var container: ModelContainer!
    var context: ModelContext!

    init() {
      url = FileManager.default.temporaryDirectory.appending(component: "default.store")
      // clean up any leftover DB files
      try? FileManager.default.removeItem(at: url)
    }

    @Test func migrateTo1_2_0() throws {
      // 1. Create a container with the OLD schema, insert test data
      container = try initModelContainer(for: OldSchema.self, with: nil, storeUrl: url)
      context = ModelContext(container)
      OldSchema.insertMocks(context: context)

      // 2. Close and reopen with the NEW schema + migration plan
      //    (this is why in-memory doesn't work — the DB must survive the close)
      container = try initModelContainer(for: NewSchema.self, with: MyMigrationPlan.self, storeUrl: url)
      context = ModelContext(container)

      let records = try context.fetch(FetchDescriptor<NewSchema.SaveData>())
      #expect(records.count == 1)
    }
  }

These tests are .serialized because they share a file path and each test writes → closes → reopens the SQLite store.

E2E tests use in-memory stores and are fully independent — each nested suite creates its own ModelContainer, so they can safely run concurrently:

  @Suite struct E2ETests {
    @Suite struct Moves {
      let session = GameSession(store: inMemoryModelContainer())
      @Test func magnetMove() async throws { /* ... */ }
      @Test func positionMove() async throws { /* ... */ }
    }

    @Suite struct Physics {
      let session = GameSession(store: inMemoryModelContainer())
      @Test func gravity() async throws { /* ... */ }
    }

    // ~12 more nested suites, all independent
  }

The conflict: if MigrationTests and E2ETests run at the same time, the file-backed migration tests can interfere with the global SwiftData state (schema registration, container caching). Today I work around this with .disabled on MigrationTests and run them separately, but what I actually want is:

  @Suite(.serialized) struct AllTests {
    @Suite(.serialized) struct MigrationTests { /* file-backed, must be serial */ }

    @Suite(.concurrent)  // ← this doesn't exist
    struct E2ETests { /* in-memory, safe to parallelize */ }
  }

The root .serialized would ensure migrations finish before E2E starts (or vice versa), while .concurrent on E2E would let its ~200 tests run in parallel as they do today. Without this, my options are either disable migration tests (current workaround) or serialize everything (which makes the E2E suite painfully slow).

Can you instead teach each test to use a different URL? i.e.:

- url = FileManager.default.temporaryDirectory.appending(component: "default.store")
+ let uuid = UUID().uuidString
+ url = FileManager.default.temporaryDirectory.appending(component: "\(uuid).store")
1 Like