with one click
testing
// Testing patterns and conventions. Use when writing unit tests, using Swift Testing framework, or following Given/When/Then structure.
// Testing patterns and conventions. Use when writing unit tests, using Swift Testing framework, or following Given/When/Then structure.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | testing |
| description | Testing patterns and conventions. Use when writing unit tests, using Swift Testing framework, or following Given/When/Then structure. |
Guide for writing tests using Swift Testing framework following project conventions.
| 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) |
| Include | Exclude |
|---|---|
Source targets (Sources/) | Mock targets (Mocks/) |
| Production code | Test targets (Tests/) |
| External libraries |
Always name the object being tested as sut:
// RIGHT
let sut = GetUserUseCase(client: mockClient)
// WRONG
let useCase = GetUserUseCase(client: mockClient)
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 { }
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)
}
// 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)
}
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:
value as the variable name for the result being testedexpected variable with the stub matching the expected output#expect(value == expected)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)
}
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.
// 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() { }
Organize tests by method name using // MARK: - sections:
// MARK: - Initial State
// MARK: - didAppear
// MARK: - didTapOnRetryButton
// MARK: - didPullToRefresh
// MARK: - didTapOnEpisodes
// MARK: - Helpers
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() { ... }
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 { }
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
{ComponentName}Tests.swift in Tests/Unit/@Test attributes include a descriptionsuttest prefix in method namesTests/Shared/Stubs/Tests/Shared/Mocks/ or Mocks/)Tests/Shared/Extensions/ for types with ErrorTests/Shared/Fixtures/ (Mapper and DataSource tests only)Tests/Shared/Resources/