home / skills / willsigmon / sigstack / ios-testing-expert

ios-testing-expert skill

/plugins/ios-dev/skills/ios-testing-expert

This skill helps you design and implement robust iOS tests using XCTest, XCUITest, and Quick/Nimble to ensure reliable app quality.

npx playbooks add skill willsigmon/sigstack --skill ios-testing-expert

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
4.6 KB
---
name: iOS Testing Expert
description: iOS testing - XCTest, XCUITest, Quick/Nimble, test patterns for Swift
allowed-tools: Read, Edit, Bash
model: sonnet
---

# iOS Testing Expert

Comprehensive testing patterns for iOS apps.

## Testing Pyramid for iOS

```
     /\        E2E (XCUITest) - 10%
    /  \       UI Tests, full flows
   /----\
  /      \     Integration - 20%
 /        \    ViewModels + Services
/----------\
/            \  Unit - 70%
              \ Models, Utils, Pure Logic
```

## XCTest Basics

### Unit Test
```swift
import XCTest
@testable import App

final class UserTests: XCTestCase {

    func testUserFullName() {
        let user = User(firstName: "John", lastName: "Doe")
        XCTAssertEqual(user.fullName, "John Doe")
    }

    func testUserValidation() throws {
        let user = User(email: "invalid")
        XCTAssertThrowsError(try user.validate())
    }
}
```

### Async Testing
```swift
func testAsyncFetch() async throws {
    let service = UserService()
    let user = try await service.fetch(id: "123")
    XCTAssertEqual(user.id, "123")
}

// Or with expectations
func testAsyncWithExpectation() {
    let expectation = expectation(description: "fetch")

    service.fetch(id: "123") { result in
        XCTAssertNotNil(result)
        expectation.fulfill()
    }

    wait(for: [expectation], timeout: 5)
}
```

## XCUITest (UI Tests)

### Basic UI Test
```swift
import XCUITest

final class LoginUITests: XCTestCase {
    let app = XCUIApplication()

    override func setUpWithError() throws {
        continueAfterFailure = false
        app.launch()
    }

    func testLogin() throws {
        app.textFields["email"].tap()
        app.textFields["email"].typeText("[email protected]")

        app.secureTextFields["password"].tap()
        app.secureTextFields["password"].typeText("password123")

        app.buttons["Login"].tap()

        XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 5))
    }
}
```

### Accessibility Identifiers
```swift
// In your SwiftUI View
TextField("Email", text: $email)
    .accessibilityIdentifier("email")

// In UI Test
app.textFields["email"].tap()
```

## Quick/Nimble (BDD Style)

### Setup
```swift
// Package.swift
.package(url: "https://github.com/Quick/Quick.git", from: "7.0.0"),
.package(url: "https://github.com/Quick/Nimble.git", from: "13.0.0"),
```

### BDD Test
```swift
import Quick
import Nimble
@testable import App

final class UserSpec: QuickSpec {
    override class func spec() {
        describe("User") {
            var user: User!

            beforeEach {
                user = User(firstName: "John", lastName: "Doe")
            }

            context("when valid") {
                it("has a full name") {
                    expect(user.fullName).to(equal("John Doe"))
                }

                it("can be validated") {
                    expect { try user.validate() }.toNot(throwError())
                }
            }

            context("when email is invalid") {
                beforeEach {
                    user.email = "invalid"
                }

                it("throws validation error") {
                    expect { try user.validate() }.to(throwError())
                }
            }
        }
    }
}
```

## Mocking with Protocols

### Protocol-Based DI
```swift
protocol UserServiceProtocol {
    func fetch(id: String) async throws -> User
}

// Real implementation
class UserService: UserServiceProtocol {
    func fetch(id: String) async throws -> User {
        // Network call
    }
}

// Mock for testing
class MockUserService: UserServiceProtocol {
    var mockUser: User?
    var shouldFail = false

    func fetch(id: String) async throws -> User {
        if shouldFail { throw APIError.failed }
        return mockUser ?? User(id: id)
    }
}
```

### Testing with Mock
```swift
func testViewModelLoadsUser() async {
    let mockService = MockUserService()
    mockService.mockUser = User(id: "1", name: "Test")

    let viewModel = UserViewModel(service: mockService)
    await viewModel.load()

    XCTAssertEqual(viewModel.user?.name, "Test")
}
```

## Snapshot Testing

### swift-snapshot-testing
```swift
import SnapshotTesting
import SwiftUI

func testUserProfileView() {
    let view = UserProfileView(user: .mock)

    assertSnapshot(
        of: view,
        as: .image(layout: .device(config: .iPhone13))
    )
}
```

## Test Coverage

### Enable Coverage
```bash
xcodebuild test \
  -scheme App \
  -enableCodeCoverage YES \
  -resultBundlePath TestResults.xcresult
```

### View Report
```bash
xcrun xccov view --report TestResults.xcresult
```

Use when: Writing iOS tests, setting up testing infrastructure, mocking

Overview

This skill bundles practical patterns and examples for testing iOS apps using XCTest, XCUITest, Quick/Nimble, snapshot testing, and protocol-based mocking. It focuses on actionable guidance: unit, integration, and UI test patterns, async testing, dependency injection for mocks, and coverage tooling. Use it to improve test reliability, speed, and maintainability across Swift projects.

How this skill works

The skill explains how to structure tests according to the testing pyramid (unit → integration → E2E) and provides concrete code examples for XCTest and XCUITest. It shows async test patterns, BDD-style tests with Quick/Nimble, snapshot testing with swift-snapshot-testing, and how to replace real services with protocol-based mocks for deterministic tests. It also covers enabling and viewing code coverage results from xcodebuild and xccov.

When to use it

  • Writing unit tests for models, utilities, and pure logic
  • Adding integration tests for view models and service interactions
  • Creating UI/end-to-end flows with XCUITest to validate user journeys
  • Introducing Quick/Nimble when you prefer BDD-style readable specs
  • Adding snapshot tests to prevent visual regressions
  • Setting up mocks and DI to isolate network and persistence layers

Best practices

  • Follow the testing pyramid: prioritize unit tests, supplement with integration and a few E2E tests
  • Use accessibility identifiers for stable UI test selectors instead of label text
  • Depend on protocols for services and inject mocks in tests to avoid network calls
  • Prefer async/await test APIs for modern async code and fall back to expectations for callback-based APIs
  • Keep UI tests deterministic and short; isolate one flow per test
  • Run coverage with xcodebuild and review xccov reports to find untested areas

Example use cases

  • Unit test a User model’s validation and computed properties with XCTest
  • Mock a UserService via a protocol and assert a ViewModel’s loading behavior in async tests
  • Write a login flow XCUITest that types credentials, taps login, and waits for a welcome label
  • Create Quick/Nimble specs for expressive BDD-style expectations and shared setup/teardown
  • Add snapshot tests for a profile SwiftUI view to catch visual regressions across device configs
  • Enable code coverage during CI runs and fail builds when critical modules lack coverage

FAQ

When should I use Quick/Nimble versus plain XCTest?

Use Quick/Nimble when you want expressive BDD-style specs and shared setup blocks; use XCTest for simple, direct unit tests to avoid extra dependencies.

How do I avoid flaky UI tests?

Stabilize selectors with accessibility identifiers, avoid animations in test runs, wait explicitly for elements with waitForExistence, and keep tests focused on a single flow.