| name | testing |
| description | Testing patterns and conventions. Use when writing unit tests, using Swift Testing framework, or following Given/When/Then structure. |
Skill: Testing
Guide for writing tests using Swift Testing framework following project conventions.
References
Testing Frameworks
| Framework | Usage |
|---|
| Testing (Swift Testing) | Unit tests, integration tests |
| ChallengeSnapshotTestKit | Snapshot tests for UI components (see /snapshot skill) |
| XCTest | UI tests (see /ui-tests skill) |
Test Coverage Requirements
- All business logic (Use Cases) must have 100% test coverage
- All ViewModels must have comprehensive test coverage
- All public API of shared modules must be tested
- UI components should have snapshot tests
Coverage Scope
| Include | Exclude |
|---|
Source targets (Sources/) | Mock targets (Mocks/) |
| Production code | Test targets (Tests/) |
| External libraries |
System Under Test (SUT)
Always name the object being tested as sut:
let sut = GetUserUseCase(client: mockClient)
let useCase = GetUserUseCase(client: mockClient)
Test Descriptions
All tests MUST include a description in the @Test attribute:
@Test("Fetches user successfully from repository")
func fetchesUserSuccessfully() async throws { }
@Test
func fetchesUserSuccessfully() async throws { }
Given / When / Then Structure
All tests must use // Given, // When, // Then comments:
@Test("Fetches user successfully from repository")
func fetchesUserSuccessfully() async throws {
let expectedUser = User(id: 1, name: "John")
let mockClient = HTTPClientMock()
mockClient.result = .success(expectedUser.encoded())
let sut = GetUserUseCase(client: mockClient)
let result = try await sut.execute(userId: 1)
#expect(result == expectedUser)
}
Assertions
#expect(value == expected)
#expect(array.isEmpty)
#expect(count > 0)
let data = try #require(response.data)
let user = try #require(users.first)
await #expect(throws: HTTPError.invalidURL) {
try await client.request(invalidEndpoint)
}
Comparing Results
Always compare full objects instead of checking individual properties:
let expected = Character.stub()
let value = try await sut.getCharacter(id: 1)
#expect(value == expected)
#expect(result.id == 1)
#expect(result.name == "Rick Sanchez")
Rules:
- Use
value as the variable name for the result being tested
- Create an
expected variable with the stub matching the expected output
- Compare with a single
#expect(value == expected)
Parameterized Tests
Always prefer @Test(arguments:) for testing multiple cases:
@Test("Endpoint supports HTTP method", arguments: [
HTTPMethod.get,
HTTPMethod.post,
HTTPMethod.put,
])
func endpointSupportsHTTPMethod(_ method: HTTPMethod) {
let path = "/test"
let sut = Endpoint(path: path, method: method)
#expect(sut.method == method)
}
Scenario-Based Parameterized Tests
For ViewModel actions with multiple outcomes (success/failure/edge cases), use scenario structs with @Test(arguments:). Each scenario defines its Given inputs and Expected outputs:
@Test("didAppear produces expected outcome per scenario", arguments: DidAppearScenario.all)
func didAppear(scenario: DidAppearScenario) async {
getUseCaseMock.result = scenario.given.result
await sut.didAppear()
#expect(sut.state == scenario.expected.state)
#expect(trackerMock.loadErrorDescriptions == scenario.expected.loadErrorDescriptions)
}
See references/test-patterns.md for scenario struct pattern and helper methods.
Test Naming
@Test("Returns correct value when input is valid")
func returnsCorrectValue() { }
@Test("Returns correct value")
func testReturnsCorrectValue() { }
MARK Organization
Organize tests by method name using // MARK: - sections:
Consolidated Assertions
Each test verifies all side effects of an action together (state, navigation, tracking) — do not split into separate tests:
@Test("didTapOnCharacterButton navigates to characters and tracks event")
func didTapOnCharacterButton() {
sut.didTapOnCharacterButton()
#expect(navigatorMock.navigateToCharactersCallCount == 1)
#expect(trackerMock.characterButtonTappedCallCount == 1)
}
@Test("didTapOnCharacterButton navigates to characters")
func didTapOnCharacterButtonNavigates() { ... }
@Test("didTapOnCharacterButton tracks event")
func didTapOnCharacterButtonTracks() { ... }
Time Limits
Use @Suite(.timeLimit(.minutes(1))) only for test suites that use async/await:
@Suite(.timeLimit(.minutes(1)))
struct GetCharacterUseCaseTests { }
struct CharacterStatusTests { }
File Structure
Tests/
├── Unit/
│ ├── Domain/UseCases/{Name}UseCaseTests.swift
│ ├── Data/Repositories/{Name}RepositoryTests.swift
│ ├── Presentation/{Screen}/ViewModels/{Screen}ViewModelTests.swift
│ └── Feature/{Feature}FeatureTests.swift
├── Snapshots/Presentation/{Screen}/{Screen}ViewSnapshotTests.swift
└── Shared/
├── Stubs/{Name}+Stub.swift
├── Mocks/{Name}Mock.swift
├── Fixtures/{name}.json
├── Extensions/{Name}+Equatable.swift
└── Resources/test-avatar.jpg
Checklist