swift-testing

Swift testing patterns: Swift Testing framework (Swift 6+), XCTest for UI tests, async/await test cases, actor testing, Combine testing, and XCUITest for UI automation. TDD for Swift/SwiftUI.

8 stars

Best use case

swift-testing is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Swift testing patterns: Swift Testing framework (Swift 6+), XCTest for UI tests, async/await test cases, actor testing, Combine testing, and XCUITest for UI automation. TDD for Swift/SwiftUI.

Teams using swift-testing should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/swift-testing/SKILL.md --create-dirs "https://raw.githubusercontent.com/marvinrichter/clarc/main/skills/swift-testing/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/swift-testing/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How swift-testing Compares

Feature / Agentswift-testingStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Swift testing patterns: Swift Testing framework (Swift 6+), XCTest for UI tests, async/await test cases, actor testing, Combine testing, and XCUITest for UI automation. TDD for Swift/SwiftUI.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Swift Testing

Core testing patterns for Swift using the Swift Testing framework, XCTest, and XCUITest.

## When to Activate

- Writing tests for Swift or SwiftUI code
- Setting up a test target in Xcode
- Testing async/actor code
- Writing UI automation tests
- Achieving 80%+ code coverage in Swift projects
- Migrating an existing XCTest suite to the Swift Testing framework (`@Test`, `#expect`)
- Debugging flaky tests caused by shared mutable state or fixed `sleep` waits
- Adding parameterized test cases to cover multiple input combinations efficiently

## Framework Selection

| Use Case | Framework | Notes |
|---|---|---|
| Unit + Integration | **Swift Testing** | Swift 6+, preferred. `@Test`, `#expect`, `@Suite` |
| Unit + Integration (legacy) | **XCTest** | Compatible with all Swift versions |
| UI Automation | **XCUITest** | Xcode Instruments + accessibility identifiers |
| Protocol-based mocking | See `swift-protocol-di-testing` | Companion skill for DI patterns |

## Swift Testing Framework (Swift 6+)

```swift
import Testing

// Basic test
@Test func formatPrice() {
    #expect(PriceFormatter.format(cents: 1000, currency: "USD") == "$10.00")
}

// Grouped suite
@Suite("PriceFormatter")
struct PriceFormatterTests {
    @Test func formatsZero() {
        #expect(PriceFormatter.format(cents: 0, currency: "USD") == "$0.00")
    }

    @Test func throwsOnNegative() throws {
        #expect(throws: PriceError.negativeAmount) {
            try PriceFormatter.format(cents: -1, currency: "USD")
        }
    }
}

// Parameterized tests
@Test("Formats various currencies", arguments: [
    (100, "USD", "$1.00"),
    (100, "EUR", "€1.00"),
    (100, "GBP", "£1.00"),
])
func formatsMultipleCurrencies(cents: Int, currency: String, expected: String) {
    #expect(PriceFormatter.format(cents: cents, currency: currency) == expected)
}
```

## Async Testing

```swift
import Testing

@Test func fetchesUser() async throws {
    let service = UserService(client: MockHTTPClient())
    let user = try await service.fetchUser(id: "123")
    #expect(user.id == "123")
    #expect(user.name == "Test User")
}

// Timeout-bounded async tests
@Test(.timeLimit(.seconds(5)))
func completesWithinTimeout() async throws {
    let result = try await slowOperation()
    #expect(result != nil)
}
```

## Actor Testing

```swift
actor Counter {
    private(set) var count = 0
    func increment() { count += 1 }
}

@Test func actorIncrements() async {
    let counter = Counter()
    await counter.increment()
    let count = await counter.count
    #expect(count == 1)
}
```

## XCTest (Legacy / UI Tests)

```swift
import XCTest

class UserServiceTests: XCTestCase {
    var sut: UserService!
    var mockClient: MockHTTPClient!

    override func setUp() {
        super.setUp()
        mockClient = MockHTTPClient()
        sut = UserService(client: mockClient)
    }

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

    func testFetchUser_returnsUser() async throws {
        mockClient.stubbedResponse = .success(UserFixtures.testUser)
        let user = try await sut.fetchUser(id: "123")
        XCTAssertEqual(user.id, "123")
    }

    func testFetchUser_throwsOnNetworkError() async {
        mockClient.stubbedResponse = .failure(URLError(.notConnectedToInternet))
        do {
            _ = try await sut.fetchUser(id: "123")
            XCTFail("Expected error to be thrown")
        } catch {
            XCTAssertTrue(error is URLError)
        }
    }
}
```

## Combine Testing

```swift
import XCTest
import Combine

class ViewModelTests: XCTestCase {
    var cancellables = Set<AnyCancellable>()

    func testPublishesUpdatedValue() {
        let vm = CounterViewModel()
        var received: [Int] = []
        let expectation = expectation(description: "receives values")
        expectation.expectedFulfillmentCount = 3

        vm.$count.sink { value in
            received.append(value)
            expectation.fulfill()
        }.store(in: &cancellables)

        vm.increment()
        vm.increment()

        waitForExpectations(timeout: 1)
        XCTAssertEqual(received, [0, 1, 2])
    }
}
```

## UI Testing with XCUITest

```swift
import XCTest

class LoginUITests: XCTestCase {
    let app = XCUIApplication()

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

    func testLoginFlow() throws {
        // Use accessibilityIdentifier set in SwiftUI with .accessibilityIdentifier()
        let emailField = app.textFields["login.email"]
        let passwordField = app.secureTextFields["login.password"]
        let loginButton = app.buttons["login.submit"]

        emailField.tap()
        emailField.typeText("user@example.com")
        passwordField.tap()
        passwordField.typeText("password123")
        loginButton.tap()

        XCTAssertTrue(app.staticTexts["dashboard.title"].waitForExistence(timeout: 3))
    }
}
```

## Coverage in Xcode

```bash
# Enable code coverage in scheme settings:
# Product → Scheme → Edit Scheme → Test → Code Coverage → ✓ Gather coverage

# Run with coverage from CLI
xcodebuild test \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -enableCodeCoverage YES | xcpretty
```

Target: **80%+ line coverage** — enforce in CI via `xccov` or `slather`.

## TDD Cycle for Swift

1. **RED**: Write `@Test func behaviour() { #expect(...) }` — it fails to compile or returns wrong value
2. **GREEN**: Write minimum Swift code to make `#expect` pass
3. **REFACTOR**: Extract protocols, use value types, clean up — all tests stay green
4. **VERIFY**: Run `swift test --enable-code-coverage`, check report

## Anti-Patterns

### Force-Unwrapping in Tests

**Wrong:**
```swift
func testFetchUser_returnsUser() async throws {
    let user = try await sut.fetchUser(id: "123")
    XCTAssertEqual(user!.id, "123") // crashes on nil, hides the real failure
}
```

**Correct:**
```swift
func testFetchUser_returnsUser() async throws {
    let user = try await sut.fetchUser(id: "123")
    XCTAssertEqual(user.id, "123") // throws and reports properly on failure
}
```

**Why:** Force-unwrapping in tests causes a crash instead of a test failure, hiding the root cause and making CI output unreadable.

### Using `sleep` to Wait for Async Results

**Wrong:**
```swift
@Test func publishesEvent() async {
    sut.triggerAsyncWork()
    try await Task.sleep(for: .seconds(1)) // arbitrary wait, brittle
    #expect(sut.didPublish == true)
}
```

**Correct:**
```swift
@Test func publishesEvent() async throws {
    let didPublish = try await withTimeout(.seconds(5)) {
        await sut.triggerAsyncWork()
        return sut.didPublish
    }
    #expect(didPublish == true)
}
```

**Why:** Fixed sleeps make tests slow and still flaky; use structured concurrency with bounded waits instead.

### Testing Implementation Details Instead of Behavior

**Wrong:**
```swift
@Test func incrementUpdatesPrivateCount() {
    let vm = CounterViewModel()
    vm.increment()
    #expect(vm._internalCount == 1) // accesses private state
}
```

**Correct:**
```swift
@Test func incrementUpdatesDisplayedValue() {
    let vm = CounterViewModel()
    vm.increment()
    #expect(vm.displayText == "1") // tests observable output
}
```

**Why:** Tests coupled to private implementation break on safe refactors; test observable behavior instead.

### Sharing Mutable State Between Tests

**Wrong:**
```swift
var sharedService = UserService(client: MockHTTPClient())

@Test func firstTest() async throws {
    sharedService.reset()
    // ... test A modifies sharedService
}

@Test func secondTest() async throws {
    // relies on sharedService state from firstTest — order-dependent
}
```

**Correct:**
```swift
@Suite("UserService")
struct UserServiceTests {
    @Test func fetchUser() async throws {
        let service = UserService(client: MockHTTPClient()) // fresh instance
        // test A
    }

    @Test func handleError() async throws {
        let service = UserService(client: MockHTTPClient()) // fresh instance
        // test B
    }
}
```

**Why:** Shared mutable state causes order-dependent flakiness; create fresh instances per test.

### Using UI Label Text Instead of Accessibility Identifiers in XCUITest

**Wrong:**
```swift
func testLoginFlow() throws {
    app.buttons["Log In"].tap() // breaks if button label is localized or renamed
}
```

**Correct:**
```swift
func testLoginFlow() throws {
    app.buttons["login.submit"].tap() // stable accessibilityIdentifier set in SwiftUI
}
```

**Why:** Querying by label text breaks with localization changes or copy rewrites; `accessibilityIdentifier` is stable and locale-independent.

## Common Pitfalls

- **Avoid `@MainActor` in test bodies**: Marks the entire test synchronous — use `await` instead
- **Always set `continueAfterFailure = false`** in UI tests to stop on first failure
- **Use `accessibilityIdentifier` not label text** for stable UI test element queries
- **Prefer `#expect` over `XCTAssert*`** in Swift Testing — better error messages and parameterization

Related Skills

visual-testing

8
from marvinrichter/clarc

Visual Regression Testing: tool comparison (Chromatic/Percy/Playwright screenshots/BackstopJS), pixel-diff vs AI-based comparison, baseline management, flakiness strategies (masks, tolerances, waitForLoadState), CI integration with GitHub Actions, and Storybook integration.

typescript-testing

8
from marvinrichter/clarc

TypeScript testing patterns: Vitest for unit/integration, Playwright for E2E, MSW for API mocking, Testing Library for React components. Core TDD methodology for TypeScript/JavaScript projects.

swiftui-patterns

8
from marvinrichter/clarc

SwiftUI architecture patterns, state management with @Observable, view composition, navigation, performance optimization, and modern iOS/macOS UI best practices.

swift-protocol-di-testing

8
from marvinrichter/clarc

Protocol-based dependency injection for testable Swift code — mock file system, network, and external APIs using focused protocols and Swift Testing.

swift-patterns

8
from marvinrichter/clarc

Core Swift patterns — value vs reference types, protocols, generics, optionals, Result, error handling, Codable, and module organization. Foundation for all Swift development.

swift-patterns-advanced

8
from marvinrichter/clarc

Advanced Swift patterns — property wrappers, result builders, Combine basics, opaque & existential types, macro system, advanced generics, and performance optimization. Extends swift-patterns.

swift-concurrency-6-2

8
from marvinrichter/clarc

Swift 6.2 Approachable Concurrency — single-threaded by default, @concurrent for explicit background offloading, isolated conformances for main actor types.

swift-actor-persistence

8
from marvinrichter/clarc

Thread-safe data persistence in Swift using actors — in-memory cache with file-backed storage, eliminating data races by design.

scala-testing

8
from marvinrichter/clarc

Scala testing with ScalaTest, MUnit, and ScalaCheck: FunSpec/FlatSpec test structure, property-based testing with forAll, mocking with MockitoSugar, Cats Effect testing with munit-cats-effect (runTest/IOSuite), ZIO Test, Testcontainers-Scala for database integration tests, and CI integration with sbt. Use when writing or reviewing Scala tests.

rust-testing

8
from marvinrichter/clarc

Rust testing patterns — unit tests with mockall, integration tests with sqlx transactions, HTTP handler testing (axum), benchmarks (criterion), property tests (proptest), fuzzing, and CI with cargo-nextest.

rust-testing-advanced

8
from marvinrichter/clarc

Advanced Rust testing anti-patterns and corrections — cfg(test) placement, expect() over unwrap(), mockall expectation ordering, executor mixing (#[tokio::test] vs block_on), PgPool isolation with

ruby-testing

8
from marvinrichter/clarc

RSpec testing patterns for Ruby and Rails — factories, mocks, request specs, feature specs, VCR, and SimpleCov coverage.