testing
Testing patterns and conventions. Use when writing unit tests, using Swift Testing framework, or following Given/When/Then structure.
SKILL.md
| Name | testing |
| Description | Testing patterns and conventions. Use when writing unit tests, using Swift Testing framework, or following Given/When/Then structure. |
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
- Test file patterns (simple, instance variables, isolation): See references/test-patterns.md
- Mocks, stubs & helpers (mock patterns, stubs, Equatable extensions): See references/mocks-and-helpers.md
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:
// RIGHT
let sut = GetUserUseCase(client: mockClient)
// WRONG
let useCase = GetUserUseCase(client: mockClient)
Test Descriptions
All tests MUST include a description in the @Test attribute:
// RIGHT
@Test("Fetches user successfully from repository")
func fetchesUserSuccessfully() async throws { }
// WRONG - Missing description
@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 {
// Given
let expectedUser = User(id: 1, name: "John")
let mockClient = HTTPClientMock()
mockClient.result = .success(expectedUser.encoded())
let sut = GetUserUseCase(client: mockClient)
// When
let result = try await sut.execute(userId: 1)
// Then
#expect(result == expectedUser)
}
Assertions
// Use #expect for assertions
#expect(value == expected)
#expect(array.isEmpty)
#expect(count > 0)
// Use #require for unwrapping (fails test if nil)
let data = try #require(response.data)
let user = try #require(users.first)
// Use #expect(throws:) for error testing
await #expect(throws: HTTPError.invalidURL) {
try await client.request(invalidEndpoint)
}
Comparing Results
Always compare full objects instead of checking individual properties:
// RIGHT - Compare full objects using stubs
let expected = Character.stub()
let value = try await sut.getCharacter(id: 1)
#expect(value == expected)
// WRONG - Checking individual properties
#expect(result.id == 1)
#expect(result.name == "Rick Sanchez")
Rules:
- Use
valueas the variable name for the result being tested - Create an
expectedvariable 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) {
// Given
let path = "/test"
// When
let sut = Endpoint(path: path, method: method)
// Then
#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 {
// Given
getUseCaseMock.result = scenario.given.result
// When
await sut.didAppear()
// Then
#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
// RIGHT - Descriptive function name, no "test" prefix
@Test("Returns correct value when input is valid")
func returnsCorrectValue() { }
// WRONG - "test" prefix
@Test("Returns correct value")
func testReturnsCorrectValue() { }
MARK Organization
Organize tests by method name using // MARK: - sections:
// MARK: - Initial State
// MARK: - didAppear
// MARK: - didTapOnRetryButton
// MARK: - didPullToRefresh
// MARK: - didTapOnEpisodes
// MARK: - Helpers
Consolidated Assertions
Each test verifies all side effects of an action together (state, navigation, tracking) — do not split into separate tests:
// RIGHT — One test per action verifying all side effects
@Test("didTapOnCharacterButton navigates to characters and tracks event")
func didTapOnCharacterButton() {
// When
sut.didTapOnCharacterButton()
// Then
#expect(navigatorMock.navigateToCharactersCallCount == 1)
#expect(trackerMock.characterButtonTappedCallCount == 1)
}
// WRONG — Separate tests for each side effect of the same action
@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:
// Async tests need time limit
@Suite(.timeLimit(.minutes(1)))
struct GetCharacterUseCaseTests { }
// Synchronous tests don't need time limit
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
- Test file named
{ComponentName}Tests.swiftinTests/Unit/ - All
@Testattributes include a description - SUT variable named
sut - All tests use Given/When/Then comments
- No
testprefix in method names - Full object comparison (not individual properties)
- Parameterized tests for multiple cases
- Stubs created for Domain Models and DTOs in
Tests/Shared/Stubs/ - Mocks placed in appropriate location (
Tests/Shared/Mocks/orMocks/) - Equatable extensions in
Tests/Shared/Extensions/for types withError - JSON fixtures in
Tests/Shared/Fixtures/(Mapper and DataSource tests only) - Test resources (images) in
Tests/Shared/Resources/