SuperAPI AI Directory

swift-testing

Testing & quality

View repository

Updated 10-15 20:31

About this skill

Swift Testing (unit + interface)

Swift Testing is the standard for unit and integration testing in new Swift code (Xcode 16+). XCUITest (XCTest) remains the UI automation layer — Swift Testing does not replace UI automation.

Stack and scope

Layer Framework When
Unit / integration / async Swift Testing Domain, ViewModels, services, parsers
UI automation XCUITest (XCTestCase) Critical flows on Simulator/device
Performance (XCTMetric) XCTest Until there is an equivalent in Swift Testing
  • import Testing in new unit tests.
  • Do not mix #expect and XCTAssert in the same test.
  • Both can live in the same project (separate targets: Unit Tests vs UI Tests).

What to prioritize

  • ViewModels / @Observable: loading, success, error, authz
  • Domain use cases and policies
  • Decoding, validation, formatters
  • Regressions (bug → test)
  • Few UI tests on paths that generate revenue or data loss (login, checkout, create core entity)

Swift Testing anatomy

import Testing

@Test("Parse cursor rejects invalid input")
func parseCursorRejectsInvalid() {
    #expect(throws: CursorError.invalid) {
        try parseCursor("%%%")
    }
}

@Test
func parseCursorAcceptsValue() throws {
    let value = try #require(try? parseCursor("10"))
    #expect(value == 10)
}
  • @Test in free functions or methods of @Suite.
  • #expect — soft assert (continues after failure when it makes sense in the test flow).
  • #require — hard unwrap/precondition; fails and stops the test.
  • Descriptive names; optional display name in @Test("...").

Suites, traits, and parallelism

@Suite("TodoViewModel", .tags(.viewModel))
struct TodoViewModelTests {
    @Test func loadEmitsReady() async { /* ... */ }
}
  • Useful traits: .tags(...), .enabled(if:), .disabled(...), .timeLimit(...).
  • Swift Testing runs in parallel by default — no shared mutable global state; use fakes per test or explicit isolation.
  • Serialization: only when unavoidable (.serialized in the suite, if available in the project's toolchain version).

Parameterized

@Test(arguments: [
    ("", false),
    ("10", true),
])
func cursorCases(input: String, ok: Bool) {
    let result = Result { try parseCursor(input) }
    #expect(result.isSuccess == ok)
}
  • Independent cases; failure should identify the argument.

Async and concurrency

@Test
func loadTodos() async throws {
    let repo = FakeTodoRepository(items: [.init(id: "1", title: "Milk")])
    let vm = TodoViewModel(repository: repo)

    await vm.load()

    #expect(vm.state == .ready(count: 1))
}
  • async/await native — no expectation XCTest for new unit tests.
  • Inject clock/repo; avoid Task.sleep to synchronize.
  • Confirmations / conditions: use the Swift Testing confirmation APIs available in the project's toolchain, not fragile sleeps.
  • @MainActor in UI/ViewModel types when production code requires it.

Fakes and isolation

struct FakeTodoRepository: TodoRepository {
    var items: [Todo] = []
    var shouldFail = false

    func all() async throws -> [Todo] {
        if shouldFail { throw RepoError.network }
        return items
    }
}
  • Prefer protocols + fakes over dynamic mocks.
  • No real network/disk in unit tests.
  • Do not share mutable singletons between parallel tests.

ViewModel / Observation

  • Test the ViewModel without hosting View SwiftUI.
  • Arrange fakes → Act (async calls) → Assert state/#expect.
  • Minimum coverage: happy path, empty, network error, forbidden/unauth if it exists.

Interface tests (XCUITest)

Separate UI Test target; classes XCTestCase:

final class LoginUITests: XCTestCase {
    var app: XCUIApplication!

    override func setUpWithError() throws {
        continueAfterFailure = false
        app = XCUIApplication()
        app.launchArguments += ["-UITests", "-reset"]
        app.launch()
    }

    func testLoginHappyPath() {
        let email = app.textFields["login.email"]
        email.tap()
        email.typeText("user@example.com")
        app.secureTextFields["login.password"].typeText("secret")
        app.buttons["login.submit"].tap()
        XCTAssertTrue(app.staticTexts["home.title"].waitForExistence(timeout: 5))
    }
}

UI rules:

  • Select by accessibility identifier stable (login.email), not by fragile localized text.
  • In SwiftUI: .accessibilityIdentifier("...") on the controls under test.
  • continueAfterFailure = false in linear flows.
  • Launch args / environment for test account, feature flags, and skip onboarding.
  • Prefer waitForExistence to sleep.
  • One flow per test; avoid monolithic suites.
  • Run on the Simulator in CI; device only when necessary.

Pyramid: many Swift Testing → few XCUITest.

Migration from unit XCTest

  1. New unit tests → Swift Testing.
  2. When touching a logic XCTest file → convert it if the cost is low.
  3. UI and performance → remain XCTest.
  4. Do not do a big bang; coexistence is expected.

Anti-patterns

  • UI test to validate domain rule (use a unit test)
  • Queries by staticTexts["Entrar"] without identifier (breaks with i18n)
  • Global state + parallel tests
  • XCTAssert inside @Test
  • Fixed sleeps; real network without contract
  • Mass UI snapshot without review (only if the team already has a flow)

Integration

  • SwiftUI UI: swiftui-development
  • Consistent style / a11y identifiers: better-mobile-interface
  • Testable MVVM: mvvm-architecture
  • Vapor backend: vapor-swift (same preference for fakes and #expect when applicable)

Completion criteria

  • New unit tests in Swift Testing (@Test / #expect / #require)
  • Risk cases covered; async without flakiness
  • Parallel-safe (no shared mutable state)
  • UI tests only in critical flows, with stable identifiers
  • Relevant suite passes in Xcode / xcodebuild test of the project scheme