# Ios Testing

> >

- **Type:** Skill
- **Install:** `agentstack add skill-koshkinvv-ios-agent-skills-ios-testing`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [koshkinvv](https://agentstack.voostack.com/s/koshkinvv)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [koshkinvv](https://github.com/koshkinvv)
- **Source:** https://github.com/koshkinvv/ios-agent-skills/tree/main/skills/ios-testing

## Install

```sh
agentstack add skill-koshkinvv-ios-agent-skills-ios-testing
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# iOS Testing Skill

## Core Rules

1. **Use Swift Testing (`@Test`, `#expect`) for ALL new unit tests** -- it is the modern framework (Xcode 16+).
2. **Keep XCTest only for UI tests and performance tests** -- Swift Testing does not support these yet.
3. Both frameworks can coexist in the same target -- migrate incrementally, never rewrite working tests.
4. Use **protocol-based dependency injection** for testability.
5. Use **`URLProtocol`** for network mocking (NOT mocking URLSession directly).
6. Use **`isStoredInMemoryOnly: true`** for SwiftData test containers.
7. Name tests descriptively: `@Test("Login succeeds with valid credentials")`.
8. One assertion per test is ideal, but pragmatic grouping is fine.
9. **Test behavior, not implementation details.**
10. Never use `sleep()` in tests -- use expectations, confirmations, or `Clock` injection.

## Framework Choice

| Test Type | Framework | Why |
|-----------|-----------|-----|
| Unit tests (new) | Swift Testing | Modern, less boilerplate, parameterized tests |
| Unit tests (existing) | XCTest | Don't rewrite working tests without reason |
| UI tests | XCTest | Swift Testing doesn't support XCUIApplication |
| Performance tests | XCTest | `measure {}` not available in Swift Testing |
| Snapshot tests | XCTest + swift-snapshot-testing | Point-Free library, XCTest integration |

## Test Organization

```
MyAppTests/                    # Unit test target
  Models/
    UserTests.swift
    OrderTests.swift
  ViewModels/
    LoginViewModelTests.swift
    ProfileViewModelTests.swift
  Services/
    APIClientTests.swift
    AuthServiceTests.swift
  Helpers/
    Mocks/
      MockAPIClient.swift
      MockAuthService.swift
    TestData/
      UserFixtures.swift
      JSONFixtures.swift
MyAppUITests/                  # UI test target
  Screens/                     # Page Objects
    LoginScreen.swift
    HomeScreen.swift
  Flows/
    OnboardingFlowTests.swift
    PurchaseFlowTests.swift
  Helpers/
    XCUIApplication+Launch.swift
```

## Quick Start: Swift Testing

```swift
import Testing
@testable import MyApp

@Suite("AuthService")
struct AuthServiceTests {
    let sut: AuthService
    let mockAPI: MockAPIClient

    init() {
        mockAPI = MockAPIClient()
        sut = AuthService(api: mockAPI)
    }

    @Test("Login succeeds with valid credentials")
    func loginSuccess() async throws {
        mockAPI.loginResult = .success(User.fixture)

        let user = try await sut.login(email: "test@example.com", password: "pass123")

        #expect(user.email == "test@example.com")
        #expect(mockAPI.loginCallCount == 1)
    }

    @Test("Login fails with invalid credentials")
    func loginFailure() async {
        mockAPI.loginResult = .failure(AuthError.invalidCredentials)

        await #expect(throws: AuthError.invalidCredentials) {
            try await sut.login(email: "bad@example.com", password: "wrong")
        }
    }

    @Test("Password validation", arguments: [
        ("short", false),
        ("validPass1!", true),
        ("nouppercase1!", false),
        ("NOLOWERCASE1!", false),
    ])
    func passwordValidation(password: String, isValid: Bool) {
        #expect(sut.isValidPassword(password) == isValid)
    }
}
```

## Quick Start: XCTest (UI Tests)

```swift
import XCTest

final class LoginUITests: XCTestCase {
    var app: XCUIApplication!

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

    func test_login_withValidCredentials_showsHome() {
        let loginScreen = LoginScreen(app: app)

        loginScreen
            .typeEmail("user@example.com")
            .typePassword("password123")
            .tapLogin()

        let homeScreen = HomeScreen(app: app)
        XCTAssertTrue(homeScreen.welcomeLabel.waitForExistence(timeout: 5))
    }
}
```

## Protocol-Based Mocking Pattern

```swift
// 1. Define protocol
protocol APIClientProtocol: Sendable {
    func login(email: String, password: String) async throws -> User
    func fetchProfile(id: String) async throws -> Profile
}

// 2. Production implementation
final class APIClient: APIClientProtocol {
    func login(email: String, password: String) async throws -> User { /* real impl */ }
    func fetchProfile(id: String) async throws -> Profile { /* real impl */ }
}

// 3. Mock for tests
final class MockAPIClient: APIClientProtocol, @unchecked Sendable {
    var loginResult: Result = .failure(TestError.notConfigured)
    var loginCallCount = 0
    var loginReceivedArgs: [(email: String, password: String)] = []

    func login(email: String, password: String) async throws -> User {
        loginCallCount += 1
        loginReceivedArgs.append((email, password))
        return try loginResult.get()
    }

    var fetchProfileResult: Result = .failure(TestError.notConfigured)
    var fetchProfileCallCount = 0

    func fetchProfile(id: String) async throws -> Profile {
        fetchProfileCallCount += 1
        return try fetchProfileResult.get()
    }
}
```

## URLProtocol Network Mocking

```swift
final class MockURLProtocol: URLProtocol {
    static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?

    override class func canInit(with request: URLRequest) -> Bool { true }
    override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }

    override func startLoading() {
        guard let handler = Self.requestHandler else {
            client?.urlProtocolDidFinishLoading(self)
            return
        }
        do {
            let (response, data) = try handler(request)
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            client?.urlProtocol(self, didLoad: data)
            client?.urlProtocolDidFinishLoading(self)
        } catch {
            client?.urlProtocol(self, didFailWithError: error)
        }
    }

    override func stopLoading() {}
}

// Usage in test:
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MockURLProtocol.self]
let session = URLSession(configuration: config)
let apiClient = APIClient(session: session)

MockURLProtocol.requestHandler = { request in
    let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
    let data = try JSONEncoder().encode(User.fixture)
    return (response, data)
}
```

## SwiftData Testing

```swift
@Test("Saving a user persists it")
func saveUser() throws {
    let config = ModelConfiguration(isStoredInMemoryOnly: true)
    let container = try ModelContainer(for: User.self, configurations: config)
    let context = ModelContext(container)

    let user = User(name: "Test", email: "test@example.com")
    context.insert(user)
    try context.save()

    let descriptor = FetchDescriptor()
    let users = try context.fetch(descriptor)
    #expect(users.count == 1)
    #expect(users.first?.name == "Test")
}
```

## Combine Testing

```swift
@Test("Publisher emits values correctly")
func publisherEmitsValues() async {
    let viewModel = CounterViewModel()
    var received: [Int] = []
    let cancellable = viewModel.$count.sink { received.append($0) }

    viewModel.increment()
    viewModel.increment()

    #expect(received == [0, 1, 2])
    cancellable.cancel()
}
```

## async/await Confirmation (Replaces XCTestExpectation)

```swift
@Test("Notification triggers callback")
func notificationCallback() async {
    await confirmation("callback received") { confirm in
        let observer = NotificationObserver {
            confirm()
        }
        NotificationCenter.default.post(name: .testNotification, object: nil)
    }
}

@Test("Delegate called exactly 3 times")
func delegateCalledThreeTimes() async {
    await confirmation("delegate called", expectedCount: 3) { confirm in
        let delegate = MockDelegate(onCall: { confirm() })
        let sut = DataLoader(delegate: delegate)
        await sut.loadBatch(count: 3)
    }
}
```

## Test Fixtures Pattern

```swift
extension User {
    static var fixture: User {
        User(id: "test-id", name: "Test User", email: "test@example.com")
    }

    static func fixture(
        id: String = "test-id",
        name: String = "Test User",
        email: String = "test@example.com"
    ) -> User {
        User(id: id, name: name, email: email)
    }
}
```

## Common Mistakes to Avoid

1. **Don't test private methods** -- test public behavior instead.
2. **Don't mock what you don't own** -- wrap third-party APIs in your own protocol.
3. **Don't use `sleep()` or `Task.sleep()` for timing** -- use `Clock` injection or expectations.
4. **Don't share mutable state between tests** -- use struct-based `@Suite` with `init()`.
5. **Don't forget `@MainActor` isolation** -- if your SUT is `@MainActor`, your test must be too.
6. **Don't use `XCTAssert` in Swift Testing** -- use `#expect` and `#require`.
7. **Don't force-unwrap in tests** -- use `#require` or `XCTUnwrap`.
8. **Don't test Apple frameworks** -- trust that `UserDefaults.set` works.
9. **Don't write tests after the fact just for coverage** -- write tests that catch real bugs.
10. **Don't ignore flaky tests** -- use `withKnownIssue` to mark them, then fix root cause.

## Migration: XCTest to Swift Testing

| XCTest | Swift Testing |
|--------|--------------|
| `class MyTests: XCTestCase` | `@Suite struct MyTests` |
| `func testSomething()` | `@Test func something()` |
| `override func setUp()` | `init()` |
| `override func tearDown()` | `deinit` |
| `XCTAssertEqual(a, b)` | `#expect(a == b)` |
| `XCTAssertNil(x)` | `#expect(x == nil)` |
| `XCTAssertThrowsError(expr)` | `#expect(throws: ErrorType.self) { expr }` |
| `XCTUnwrap(optional)` | `try #require(optional)` |
| `XCTestExpectation` + `wait` | `confirmation { }` |
| `XCTSkipIf(condition)` | `.enabled(if: !condition)` trait |
| `measure { }` | No equivalent -- keep in XCTest |
| UI tests with XCUIApplication | No equivalent -- keep in XCTest |

## References

- [references/swift-testing.md](references/swift-testing.md) -- Swift Testing framework deep dive
- [references/xctest.md](references/xctest.md) -- XCTest assertions, async, performance
- [references/ui-testing.md](references/ui-testing.md) -- XCUIApplication, Page Object, accessibility
- [references/mocking.md](references/mocking.md) -- Protocol mocks, URLProtocol, snapshot testing

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [koshkinvv](https://github.com/koshkinvv)
- **Source:** [koshkinvv/ios-agent-skills](https://github.com/koshkinvv/ios-agent-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-koshkinvv-ios-agent-skills-ios-testing
- Seller: https://agentstack.voostack.com/s/koshkinvv
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
