To make XCTest cases pass individually and as a suite, give each test its own mutable fixture and inject it into the code under test. When a singleton must remain shared, reset it before every test and prevent overlapping access throughout the test’s lifetime.

This guide follows one Mac development task: fixing layout-calculation tests in Xcode that pass alone but fail together. The example uses only synthetic integer widths and local arithmetic. It requires no app window, service mock, file storage, or network connection.

Why can a test pass alone and fail in the suite?

A test run in isolation may encounter a singleton’s initial state. In a suite, an earlier test may already have changed it. A new reference to that singleton still points to the same mutable object.

Swift distinguishes instance properties from type properties: each instance has its own instance properties, while a type property has one shared copy. Stored type properties initialize once on first access. Consequently, assigning SpacingCalculator.shared to a test property does not create a fresh calculator. See the Swift project’s Properties documentation.

Consider a synthetic row containing items with widths [10, 20, 30]. Its total width is the sum of the item widths plus the gaps between them. With a default gap of 2, the result is 64; with a gap of 8, it is 76.

Suppose one test checks the default width without configuring the calculator, while another changes the shared gap to 8. The following table describes sequential execution, starting each row with a new calculator whose gap is 2. These are illustrative calculations, not recorded Xcode test results.

Execution Shared calculator without reset Fresh calculator per test
Default test alone 64: passes 64: passes
Wide-gap test alone 76: passes 76: passes
Default, then wide-gap Both pass Both pass
Wide-gap, then default Default receives 76: fails Both pass

This gives you a useful diagnostic: inspect which test changes the state used by the failing assertion. Changing test names to obtain a favorable order leaves the dependency in place.

Which XCTest lifecycle method should own the fixture?

Use an instance setup method for mutable state that every test needs. Apple documents instance setup before each test method, whereas class-level setup runs once before the test class. A mutable fixture created in class-level setup therefore does not receive a fresh baseline between methods. See Set Up and Tear Down State in Your Tests.

Choose the setup method that matches the work: synchronous, nonthrowing fixture construction fits setUp(); throwing construction fits setUpWithError(); asynchronous preparation fits setUp() async throws. Use teardown to finish the current test’s cleanup. Apple explicitly advises against depending on teardown to prepare subsequent tests, because a crash can prevent teardown from running. The same lifecycle documentation describes these boundaries.

The fixture decision depends on both ownership and execution scope:

Situation Appropriate approach Condition to check
Tests can construct the dependency Create and inject an instance per test The consumer must use that injected instance
Tests must exercise the actual singleton Establish a complete baseline before each test No overlapping test may alter that singleton
Shared configuration is immutable Reuse the immutable value Referenced objects must also remain unchanged
A test starts asynchronous work Await completion before ending the test No unfinished work may mutate the fixture afterward

The first option is the simplest for this calculator. Production code can retain a shared default while tests explicitly choose independent instances.

Build an independently owned XCTest fixture

Use a Mac with Xcode and an existing macOS unit-test target. Add the following complete example as one Swift file in that test target. It defines its own calculator, consumer, and tests, so it needs no application-module import. Use a Swift 6 toolchain with the target’s default actor isolation set to nonisolated.

The calculator is an actor to make its mutable state’s concurrency boundary explicit. Swift actors restrict access to their mutable state, and calls from outside the actor use await. Those rules are documented in the Swift project’s Concurrency chapter. Independent fixture ownership remains necessary even with an actor.

import XCTest

actor SpacingCalculator {
    static let shared = SpacingCalculator()

    private var gap = 2

    func setGap(_ value: Int) {
        precondition(value >= 0)
        gap = value
    }

    func width(of itemWidths: [Int]) -> Int {
        precondition(itemWidths.allSatisfy { $0 >= 0 })
        let gapCount = max(itemWidths.count - 1, 0)
        return itemWidths.reduce(0, +) + gapCount * gap
    }
}

struct RowLayout {
    let calculator: SpacingCalculator

    init(calculator: SpacingCalculator = .shared) {
        self.calculator = calculator
    }

    func width(of itemWidths: [Int]) async -> Int {
        await calculator.width(of: itemWidths)
    }
}

final class RowLayoutTests: XCTestCase {
    private var calculator: SpacingCalculator?

    override func setUp() {
        super.setUp()
        calculator = SpacingCalculator()
    }

    override func tearDown() {
        calculator = nil
        super.tearDown()
    }

    func testDefaultGap() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10, 20, 30])

        XCTAssertEqual(actual, 64)
    }

    func testWideGap() async throws {
        let fixture = try XCTUnwrap(calculator)
        await fixture.setGap(8)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10, 20, 30])

        XCTAssertEqual(actual, 76)
    }

    func testEmptyRow() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [])

        XCTAssertEqual(actual, 0)
    }

    func testSingleItem() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10])

        XCTAssertEqual(actual, 10)
    }
}

The decisive line is calculator = SpacingCalculator(). Every setup constructs an independent actor with gap 2. Each test then passes that actor into RowLayout. Calling RowLayout() inside these tests would select .shared and bypass the fixture.

The example deliberately allows direct construction. If your existing singleton has a private initializer, introduce an internal construction path or extract its mutable calculation state into a separately constructible dependency. An internal initializer can be exposed to a test target through the application module’s established testable-import arrangement; making the singleton reference globally replaceable is unnecessary.

The arithmetic also defines two useful edge cases: an empty row has width 0, and one item has no intervening gap. The example accepts nonnegative integers small enough to avoid integer overflow; negative values violate its preconditions and terminate execution rather than producing an assertion failure.

The expected arithmetic and both two-test orderings were checked locally with deterministic Python calculations. The Swift examples were inspected but not compiled or executed because this research environment has no Swift toolchain. Run them in Xcode to verify compilation and execution for your target.

When is resetting the singleton sufficient?

Resetting is a workable transitional approach when tests genuinely need the shared instance and its entire test-visible state can be restored. Establish the baseline before every test, including values that a test assumes are defaults. Resetting only the property changed by the currently failing test can leave other dependencies undiscovered.

For this calculator, asynchronous setup could select SpacingCalculator.shared and await setGap(2) before each method. That establishes the arithmetic baseline only if no overlapping caller changes the gap before the assertion. If the fixture later gains cached results or additional configuration, its baseline operation must cover those values too.

An actor does not grant one test ownership across several awaited calls. For example, test A can set the shared gap to 2, test B can set it to 8, and test A can then calculate a width using 8. Each actor operation can execute correctly while the overall test assumption fails. This follows from Swift’s documented actor execution and suspension rules.

Before choosing reset-based isolation, identify every test and background task that can reach the shared object. If you cannot ensure exclusive use throughout preparation, execution, and cleanup, inject independent state instead. Restoring a snapshot afterward cannot prevent another caller from observing temporary changes during the test.

How does the fixture translate to Swift Testing?

Swift Testing initializes a distinct suite instance for each instance test function. An instance property that constructs a calculator therefore provides the same ownership pattern without XCTest setup overrides. A property initialized from .shared would still reference the singleton. Apple documents the instance lifecycle in Organizing test functions with suite types.

With the earlier SpacingCalculator and RowLayout definitions present once in the same test target, this is a complete additional Swift Testing suite. It requires a toolchain that includes the Testing module, such as Xcode 16 or later. Its default actor isolation should also be nonisolated.

import Testing

struct RowLayoutSwiftTests {
    let calculator = SpacingCalculator()

    @Test func defaultGap() async {
        let layout = RowLayout(calculator: calculator)
        let actual = await layout.width(of: [10, 20, 30])
        #expect(actual == 64)
    }

    @Test func wideGap() async {
        await calculator.setGap(8)
        let layout = RowLayout(calculator: calculator)
        let actual = await layout.width(of: [10, 20, 30])
        #expect(actual == 76)
    }
}

For more involved preparation, Swift Testing supports suite initializers, including asynchronous or throwing initializers. Apple’s Migrating a test from XCTest explains this replacement for XCTest setup methods. This fixture needs no explicit cleanup because it owns only local calculation state and starts no background work.

Swift Testing runs tests in parallel by default. Applying .serialized to a suite runs its contained tests and nested suites serially, but does not prevent unrelated tests from running alongside it. Applying the trait to an ordinary nonparameterized test function has no effect. These limits are explicit in Running tests serially or in parallel.

Consequently, placing two singleton-dependent suites under separate .serialized annotations does not establish mutual exclusion between them. A common serialized parent can coordinate its descendants, provided every competing user belongs to that scope. Fresh fixtures avoid that coordination requirement for these calculation tests.

Verify the individual and suite outcomes in Xcode

Use the same scheme, test plan, and Mac destination throughout verification. Apple documents running individual methods, suites, and the active test plan in Running tests and interpreting results.

  1. Run each of the four XCTest methods individually using its editor-gutter play control. Expect 64, 76, 0, and 10, respectively.
  2. Run the RowLayoutTests class using the play control beside its declaration. All four methods should pass together.
  3. Choose Product > Test to run the active test plan. Confirm these tests actually ran rather than being skipped or excluded.
  4. If you added the Swift Testing example, run its two functions individually and together as well.

If individual tests pass but the full plan fails, trace the object used by the failing consumer. Look for a missed injection, a helper that calls .shared, or asynchronous work that continues after its originating test. If a test fails alone, check its inputs and expected calculation before attributing the failure to suite ordering.

The finished fixture should make its starting state visible in the current test’s construction path. For these layout calculations, every test owns a calculator, explicitly supplies it to the consumer, and awaits its work. That makes the expected result independent of which neighboring test ran first.

Sources