xctest-patterns

XCTest Patterns — Expert Decisions

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "xctest-patterns" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-xctest-patterns

XCTest Patterns — Expert Decisions

Expert decision frameworks for testing choices. Claude knows XCTest syntax — this skill provides judgment calls for test strategy and mock design.

Decision Trees

Test Type Selection

What are you testing? ├─ Pure business logic (no I/O, no UI) │ └─ Unit test │ Fast, isolated, many of these │ ├─ Component interactions (services, repositories) │ └─ Integration test │ Test real interactions, fewer than unit │ ├─ User-visible behavior │ └─ Does it require visual verification? │ ├─ YES → Snapshot test or manual QA │ └─ NO → UI test (XCUITest) │ Slowest, fewest of these │ └─ Performance characteristics └─ Performance test with measure {}

Mock vs Stub vs Spy

What do you need from the test double? ├─ Just return canned data │ └─ Stub │ Simplest, no verification │ ├─ Verify interactions (was method called?) │ └─ Spy │ Records calls, verifiable │ └─ Both return data AND verify calls └─ Mock (stub + spy) Most flexible, most complex

When to Mock

Is this dependency... ├─ External (network, database, filesystem)? │ └─ Always mock │ Tests must be fast and deterministic │ ├─ Internal but slow or stateful? │ └─ Mock if it makes test significantly faster │ └─ Internal and fast? └─ Consider using real implementation Integration coverage > isolation purity

Async Test Strategy

Is the async operation... ├─ Returning a value (async/await)? │ └─ Use async test function │ func testFetch() async throws { } │ ├─ Using completion handlers? │ └─ Use XCTestExpectation │ expectation.fulfill() in callback │ └─ Publishing via Combine? └─ Use XCTestExpectation + sink Or use async-aware Combine helpers

NEVER Do

Test Design

NEVER test implementation details:

// ❌ Testing internal state func testLogin() async { await sut.login(email: "test@example.com", password: "pass")

XCTAssertEqual(sut.authService.callCount, 1)  // Implementation detail!
XCTAssertEqual(sut.lastRequestTimestamp, Date())  // Internal state!

}

// ✅ Test observable behavior func testLoginSuccess_SetsAuthenticatedState() async { await sut.login(email: "test@example.com", password: "pass")

XCTAssertTrue(sut.isAuthenticated)  // Observable state

}

NEVER write tests that depend on execution order:

// ❌ Tests depend on each other func testA_AddItem() { sut.add(item) // sut state modified XCTAssertEqual(sut.count, 1) }

func testB_RemoveItem() { // Depends on testA running first! sut.remove(item) XCTAssertEqual(sut.count, 0) }

// ✅ Each test sets up its own state func testRemoveItem() { sut.add(item) // Explicit setup sut.remove(item) XCTAssertEqual(sut.count, 0) }

NEVER use sleep() in tests:

// ❌ Flaky and slow func testAsyncOperation() { sut.startOperation() Thread.sleep(forTimeInterval: 2.0) // Arbitrary wait! XCTAssertTrue(sut.isComplete) }

// ✅ Use expectations or async/await func testAsyncOperation() async { await sut.startOperation() XCTAssertTrue(sut.isComplete) }

// Or with expectations func testAsyncOperation() { let expectation = expectation(description: "Operation completes") sut.startOperation { expectation.fulfill() } wait(for: [expectation], timeout: 5.0) }

Mock Design

NEVER create mocks that have real side effects:

// ❌ Mock does real work final class MockNetworkService: NetworkServiceProtocol { func fetch(_ url: URL) async throws -> Data { try await URLSession.shared.data(from: url).0 // Real network! } }

// ✅ Mock returns stubbed data final class MockNetworkService: NetworkServiceProtocol { var stubbedData: Data = Data() var stubbedError: Error?

func fetch(_ url: URL) async throws -> Data {
    if let error = stubbedError { throw error }
    return stubbedData
}

}

NEVER verify everything in mocks:

// ❌ Over-specified — breaks if implementation changes func testLogin() async { await sut.login()

XCTAssertEqual(mockService.loginCallCount, 1)
XCTAssertEqual(mockService.setTokenCallCount, 1)
XCTAssertEqual(mockService.logAnalyticsCallCount, 1)
XCTAssertEqual(mockService.updateUserCallCount, 1)
// 10 more assertions...

}

// ✅ Verify only what matters for this test func testLogin_StoresToken() async { await sut.login() XCTAssertNotNil(mockTokenStore.storedToken) }

Async Testing

NEVER forget to await async operations:

// ❌ Test passes before async work completes func testFetchUser() { Task { await sut.fetchUser() // Runs after test ends! } XCTAssertNotNil(sut.user) // Always fails }

// ✅ Make test function async func testFetchUser() async { await sut.fetchUser() XCTAssertNotNil(sut.user) }

NEVER forget to fulfill expectations:

// ❌ Test hangs if error path doesn't fulfill func testNetworkCall() { let expectation = expectation(description: "Call completes")

sut.fetch { result in
    if case .success = result {
        expectation.fulfill()
    }
    // Error case doesn't fulfill — test hangs!
}

wait(for: [expectation], timeout: 5.0)

}

// ✅ Fulfill in all paths func testNetworkCall() { let expectation = expectation(description: "Call completes")

sut.fetch { result in
    // Always fulfill, assert after
    expectation.fulfill()
}

wait(for: [expectation], timeout: 5.0)
// Assert on result here

}

UI Testing

NEVER use fixed delays in UI tests:

// ❌ Flaky — element might appear faster or slower func testLoginFlow() { app.buttons["login"].tap() Thread.sleep(forTimeInterval: 3.0) XCTAssertTrue(app.staticTexts["Welcome"].exists) }

// ✅ Use waitForExistence func testLoginFlow() { app.buttons["login"].tap() let welcome = app.staticTexts["Welcome"] XCTAssertTrue(welcome.waitForExistence(timeout: 5.0)) }

NEVER rely on element positions:

// ❌ Breaks if UI layout changes let firstButton = app.buttons.element(boundBy: 0)

// ✅ Use accessibility identifiers let loginButton = app.buttons["loginButton"]

Essential Patterns

Structured Mock with Spy

final class MockUserService: UserServiceProtocol { // Stubs var stubbedUser: User? var stubbedError: Error?

// Spy tracking
private(set) var fetchUserCallCount = 0
private(set) var fetchUserLastId: String?

func fetchUser(id: String) async throws -> User {
    fetchUserCallCount += 1
    fetchUserLastId = id

    if let error = stubbedError { throw error }
    guard let user = stubbedUser else {
        throw MockError.notConfigured
    }
    return user
}

// Verification helpers
func verify(fetchUserCalledWith id: String) -> Bool {
    fetchUserLastId == id
}

}

ViewModel Test Pattern

@MainActor final class UserViewModelTests: XCTestCase { var sut: UserViewModel! var mockService: MockUserService!

override func setUp() {
    super.setUp()
    mockService = MockUserService()
    sut = UserViewModel(userService: mockService)
}

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

// Test initial state
func testInitialState() {
    XCTAssertNil(sut.user)
    XCTAssertFalse(sut.isLoading)
    XCTAssertNil(sut.errorMessage)
}

// Test success path
func testFetchUser_Success() async {
    mockService.stubbedUser = User(id: "1", name: "John")

    await sut.fetchUser(id: "1")

    XCTAssertEqual(sut.user?.name, "John")
    XCTAssertFalse(sut.isLoading)
    XCTAssertNil(sut.errorMessage)
}

// Test error path
func testFetchUser_Error() async {
    mockService.stubbedError = NetworkError.timeout

    await sut.fetchUser(id: "1")

    XCTAssertNil(sut.user)
    XCTAssertFalse(sut.isLoading)
    XCTAssertNotNil(sut.errorMessage)
}

}

Test Data Builder

final class UserBuilder { private var id = "test-id" private var name = "Test User" private var email = "test@example.com" private var isActive = true

func withId(_ id: String) -> Self {
    self.id = id
    return self
}

func withName(_ name: String) -> Self {
    self.name = name
    return self
}

func inactive() -> Self {
    self.isActive = false
    return self
}

func build() -> User {
    User(id: id, name: name, email: email, isActive: isActive)
}

}

// Usage let activeUser = UserBuilder().build() let inactiveUser = UserBuilder().inactive().build() let specificUser = UserBuilder().withId("123").withName("John").build()

Async Expectation Helper

extension XCTestCase { func awaitPublisher<T: Publisher>( _ publisher: T, timeout: TimeInterval = 1.0, file: StaticString = #file, line: UInt = #line ) throws -> T.Output where T.Failure == Never { var result: T.Output? let expectation = expectation(description: "Awaiting publisher")

    let cancellable = publisher.sink { value in
        result = value
        expectation.fulfill()
    }

    wait(for: [expectation], timeout: timeout)
    cancellable.cancel()

    return try XCTUnwrap(result, file: file, line: line)
}

}

Quick Reference

Test Pyramid Distribution

Test Type Quantity Speed Reliability

Unit Many (70%) Fast High

Integration Some (20%) Medium Medium

UI Few (10%) Slow Lower

Coverage Thresholds by Layer

Layer Target Rationale

Domain/Business Logic 90%+ Critical correctness

Services/Repositories 85% Error handling matters

ViewModels 70-80% State transitions

Views Don't measure Visual QA instead

Assertion Cheat Sheet

Check Assertion

Equality XCTAssertEqual(a, b)

Nil XCTAssertNil(x) / XCTAssertNotNil(x)

Boolean XCTAssertTrue(x) / XCTAssertFalse(x)

Throws XCTAssertThrowsError(try expr)

No throw XCTAssertNoThrow(try expr)

Fail XCTFail("message")

Red Flags

Smell Problem Fix

Tests run > 10s Too slow More mocking, fewer UI tests

Tests fail randomly Flaky Remove timing dependencies

setUp() is huge Tests coupled Extract builders/helpers

Mock verifies everything Over-specified Only verify what matters

Tests share state Order-dependent Fresh sut in setUp()

sleep() in tests Unreliable Expectations or async

100% coverage goal Chasing metrics Focus on behavior coverage

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

rails localization (i18n) - english & arabic

No summary provided by upstream source.

Repository SourceNeeds Review