Testing Mastery

XCTest vs Swift Testing: The Modern Approach

January 19, 2026
5 min read
Featured image for blog post: XCTest vs Swift Testing: The Modern Approach

XCTest vs Swift Testing: The Modern Approach

Apple introduced Swift Testing at WWDC 2024, and it's a game-changer for how we write tests. Let's see why you should consider migrating.

Follow along with the code: iOS-Practice on GitHub

The Old Way: XCTest

Here's what XCTest looks like—you've probably written this a hundred times:

import XCTest
@testable import MyApp

class WeatherTests: 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")
    }
}

Classes, inheritance, implicitly unwrapped optionals, setup/teardown boilerplate. It works, but it's verbose.

The New Way: Swift Testing

Same test, new framework:

import Testing
@testable import MyApp

struct WeatherTests {
    let mockService = MockWeatherService()
    let sut: WeatherManager

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

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

No inheritance. No optionals. No teardown. Just a struct with an initializer.

Assertions: Natural Swift Expressions

XCTest has dozens of assertion functions:

// XCTest
XCTAssertEqual(weather.temp, 72)
XCTAssertTrue(isLoaded)
XCTAssertNil(error)
XCTAssertNotNil(result)
XCTAssertGreaterThan(count, 0)

Swift Testing uses #expect with standard Swift expressions:

// Swift Testing
#expect(weather.temp == 72)
#expect(isLoaded)
#expect(error == nil)
#expect(result != nil)
#expect(count > 0)

You don't need to remember XCTAssertGreaterThanOrEqual—just write #expect(count >= 0).

Error Testing: Finally Clean

Testing that code throws an error in XCTest is awkward:

// XCTest
func testThrowsCityNotFound() async {
    do {
        _ = try await sut.getWeather(for: "FakeCity")
        XCTFail("Expected error")
    } catch {
        XCTAssertEqual(error as? WeatherError, .cityNotFound)
    }
}

Swift Testing handles this elegantly:

// Swift Testing
@Test func throwsCityNotFound() async {
    await #expect(throws: WeatherError.cityNotFound) {
        try await sut.getWeather(for: "FakeCity")
    }
}

One line instead of do/catch/XCTFail boilerplate.

Quick Reference

XCTestSwift Testing
import XCTestimport Testing
class Foo: XCTestCasestruct Foo
func testBar()@Test func bar()
XCTAssertEqual(a, b)#expect(a == b)
XCTAssertTrue(x)#expect(x)
XCTFail("msg")Issue.record("msg")
throw XCTSkip()throw Skip("reason")
setUp()init()

Can You Mix Them?

Yes! You can use both frameworks in the same project. This makes gradual migration practical—write new tests with Swift Testing while keeping existing XCTest tests working.

Interview tip: "Swift Testing lets me write tests faster with less boilerplate. I use #expect with natural Swift expressions instead of memorizing XCTAssert variants, and init() instead of setUp/tearDown lifecycle methods."

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