Testing Mastery

Swift Testing Setup and Teardown: Just Use init()

January 19, 2026
5 min read
Featured image for blog post: Swift Testing Setup and Teardown: Just Use init()

Swift Testing Setup and Teardown: Just Use init()

XCTest's setUp() and tearDown() lifecycle methods feel ceremonial. Swift Testing replaces them with something you already know: initializers.

Follow along with the code: iOS-Practice on GitHub

XCTest: The Ceremony

Here's the classic XCTest setup pattern:

class WeatherManagerTests: XCTestCase {
    var mockService: MockWeatherService!
    var sut: WeatherManager!

    override func setUp() {
        super.setUp()
        mockService = MockWeatherService()
        sut = WeatherManager(service: mockService)
    }

    override func tearDown() {
        sut = nil
        mockService = nil
        super.tearDown()
    }

    func testFetchReturnsData() async throws {
        let weather = try await sut.getWeather(for: "London")
        XCTAssertEqual(weather.city, "London")
    }
}

Notice the implicitly unwrapped optionals (!), the super calls, and the tearDown that just sets things to nil. This is boilerplate.

Swift Testing: Just Use init()

The same tests in Swift Testing:

struct WeatherManagerTests {
    let mockService: MockWeatherService
    let sut: WeatherManager

    init() {
        mockService = MockWeatherService()
        sut = WeatherManager(service: mockService)
    }

    @Test func fetchReturnsData() async throws {
        let weather = try await sut.getWeather(for: "London")
        #expect(weather.city == "London")
    }
}

No optionals. No forced unwrapping. No tearDown. Each test gets a fresh instance of the struct, so init() runs before every test automatically.

Why No Teardown?

Swift Testing creates a new instance of your test struct for each test. When the test completes, that instance goes out of scope and is deallocated. ARC handles cleanup automatically.

For most tests, this means teardown is simply unnecessary.

When You Actually Need Cleanup

Sometimes you have external resources that need explicit cleanup—temporary files, database connections, or resources that don't participate in ARC. Use a helper class:

final class TestResources {
    let tempDirectory: URL

    init() {
        tempDirectory = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString)
        try! FileManager.default.createDirectory(
            at: tempDirectory,
            withIntermediateDirectories: true
        )
    }

    deinit {
        try? FileManager.default.removeItem(at: tempDirectory)
    }
}

struct FileSystemTests {
    let resources = TestResources()

    @Test func writesFile() throws {
        let file = resources.tempDirectory.appendingPathComponent("test.txt")
        try "Hello".write(to: file, atomically: true, encoding: .utf8)
        #expect(FileManager.default.fileExists(atPath: file.path))
    }
}

The helper class's deinit runs when the test struct is deallocated.

Async Setup

Need async setup? Use an async initializer isn't available, but you can use a static factory method with @Test:

struct WeatherTests {
    let preloadedCache: [String: Weather]

    init() async throws {
        // Can't do async init directly, but tests can be async
        preloadedCache = [:]
    }
}

For complex async setup, consider doing it in the test itself:

@Test func fetchWithPreloadedCache() async throws {
    // Setup
    let sut = await WeatherManager.withPreloadedCache()

    // Test
    let weather = try await sut.getWeather(for: "London")
    #expect(weather.city == "London")
}

Suite-Level Setup

Suites can share expensive setup across tests:

@Suite struct DatabaseTests {
    // This is created once per suite, not per test
    static let sharedDatabase = TestDatabase()

    @Test func insertsRecord() async throws {
        // Uses Self.sharedDatabase
    }

    @Test func queriesRecord() async throws {
        // Uses Self.sharedDatabase
    }
}

Use static for truly shared resources, and instance properties for per-test isolation.

Migration Summary

XCTestSwift Testing
override func setUp()init()
override func tearDown()Not needed (ARC)
setUpWithError() throwsinit() throws
Implicitly unwrapped optionalsRegular let properties
InheritanceStruct composition

Interview tip: "Swift Testing's approach to setup is refreshingly simple—I just use init() like any other Swift type. Each test gets a fresh instance, so I don't need tearDown. If I have external resources that need cleanup, I wrap them in a helper class with deinit."

Originally published on deadpixel.llc

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch