Testing with XCTest and Swift Testing

Test targets, the expect and require macros, async tests, parameterised cases, protocol-based mocks, UI tests and running everything in CI.

Swift Testing

import Testing

struct PriceTests {
    @Test("applies discount before tax")
    func discountBeforeTax() {
        let calculator = PriceCalculator(taxRate: 0.2)
        #expect(calculator.total(unitPrice: 100, quantity: 3, discount: 0.1) == 324)
    }

    @Test("rejects negative quantities", arguments: [-1, -50, Int.min])
    func rejectsNegative(quantity: Int) {
        let calculator = PriceCalculator(taxRate: 0.2)
        #expect(throws: PriceError.self) {
            try calculator.total(unitPrice: 100, quantity: quantity, discount: 0)
        }
    }

    @Test("round trips through JSON")
    func codableRoundTrip() throws {
        let original = Article.fixture()
        let data = try JSONEncoder().encode(original)
        let decoded = try JSONDecoder().decode(Article.self, from: data)
        #expect(decoded == original)
    }

    @Test(.timeLimit(.minutes(1)))
    @MainActor
    func viewModelPublishesAfterLoad() async {
        let model = FeedViewModel(api: StubAPI(articles: [.fixture()]))
        await model.load()
        let count = model.articles.count
        #expect(count == 1)
    }
}
  • #expect records a failure and continues the test; #require unwraps an optional or throws to stop immediately.
  • Tests run in parallel by default, so a test must not depend on another test having run.
  • Parameterised arguments cover a table of inputs without duplicated method bodies.
  • @Suite groups related tests and can carry traits, such as serialising a suite that touches shared files.

XCTest, mocks and UI tests

import XCTest

final class ArticleRepositoryTests: XCTestCase {
    func testFallsBackToCacheWhenOffline() async throws {
        let api = StubAPI(error: URLError(.notConnectedToInternet))
        let cache = InMemoryCache(articles: [.fixture(id: 1)])
        let repository = ArticleRepository(api: api, cache: cache)

        let articles = try await repository.recent()

        XCTAssertEqual(articles.map(\.id), [1])
    }

    func testMeasureDecode() throws {
        let data = try Fixture.data(named: "articles.json")
        measure { _ = try? JSONDecoder().decode([Article].self, from: data) }
    }
}

final class CheckoutUITests: XCTestCase {
    func testAddToCartUpdatesTheBadge() {
        let app = XCUIApplication()
        app.launchArguments = ["-uiTestSeed", "empty"]
        app.launch()

        app.buttons["Add to cart"].firstMatch.tap()
        let badge = app.staticTexts["cart-count"]
        XCTAssertTrue(badge.waitForExistence(timeout: 5))
        XCTAssertEqual(badge.label, "1")
    }
}
FrameworkStyleBest for
Swift TestingMacros and value typesNew unit and integration tests
XCTestClass-based with assertionsUI tests and existing suites
Both togetherMixed in one targetIncremental migration

A protocol and a stub is usually enough. Reach for a generated mock only when you need to verify call order or argument values across many interactions, and even then prefer asserting on observable outcomes.

Running tests in CI

# packages: fast, no simulator needed
swift test --parallel --enable-code-coverage

# Apple apps: a specific simulator, result bundle for the report
xcodebuild test \
  -scheme App -destination 'platform=iOS Simulator,name=iPhone 16' \
  -enableCodeCoverage YES -resultBundlePath build/TestResults.xcresult

xcrun xccov view --report --json build/TestResults.xcresult > coverage.json
⚠️
An async test that awaits a real network call is flaky by construction. Inject a stub client, use a virtual clock for anything time based, and keep a single end-to-end smoke test that is allowed to be slower and is retried rather than trusted.

FAQ

Should I migrate from XCTest to Swift Testing?
Write new tests with Swift Testing and migrate the unit tests that benefit from parameterisation or the cleaner assertions. UI tests stay in XCTest for now, and both can live in the same target during the transition.
How do I test code that uses <code>Date.now</code> directly?
Inject a clock protocol with a now property and use a fixed implementation in tests. Code that reads the system clock inline cannot be tested deterministically without freezing time globally.

Networking, persistence and Codable Packaging, build configuration and distribution

Last refreshed 2026-09-18.