| name | writing-unit-tests |
| description | Writes unit tests using XCTest and Cuckoo mocks following project testing patterns. Use when creating or adding tests. |
Write Unit Tests
Writes unit tests following ShopHelp's XCTest + Cuckoo patterns.
File Location
Tests mirror the main app structure in ShopHelpTests/:
| Source | Test |
|---|
ShopHelp/Presentation/{Feature}/{Feature}ViewModel.swift | ShopHelpTests/Presentation/{Feature}ViewModelTests.swift |
ShopHelp/Domain/UseCases/{Action}{Entity}UseCase.swift | ShopHelpTests/Domain/{Action}{Entity}UseCaseTests.swift |
ShopHelp/Data/Repositories/{Entity}RepositoryImpl.swift | ShopHelpTests/Data/{Entity}RepositoryTests.swift |
Test Class Template
import XCTest
import Cuckoo
@testable import ShopHelp
@MainActor
final class {Name}Tests: XCTestCase {
private var sut: {ClassUnderTest}!
private var mockDependency: Mock{DependencyProtocol}!
override func setUp() {
super.setUp()
mockDependency = Mock{DependencyProtocol}()
sut = {ClassUnderTest}(dependency: mockDependency)
}
override func tearDown() {
sut = nil
mockDependency = nil
super.tearDown()
}
func test_{methodName}_{scenario}_{expectedResult}() async {
stub(mockDependency) { stub in
when(stub.someMethod()).thenReturn(expectedValue)
}
await sut.methodUnderTest()
XCTAssertEqual(sut.state, .loaded(expectedValue))
verify(mockDependency).someMethod()
}
}
Test Naming Convention
test_{methodName}_{scenario}_{expectedResult}
Examples:
test_loadData_success_stateIsLoaded
test_loadData_networkError_stateIsError
test_addToCart_emptyCart_addsItem
test_execute_invalidInput_throwsError
Cuckoo Mock Patterns
Stubbing
stub(mockRepository) { stub in
when(stub.fetchProduct(id: any())).thenReturn(mockProduct)
}
stub(mockRepository) { stub in
when(stub.fetchProduct(id: any())).thenThrow(NetworkError.timeout)
}
stub(mockUseCase) { stub in
when(stub.execute()).thenReturn(mockResult)
}
Verification
verify(mockRepository).fetchProduct(id: equal(to: "123"))
verify(mockRepository, times(2)).fetchProduct(id: any())
verify(mockRepository, never()).deleteProduct(id: any())
Mock Setup
Mocks are generated by Cuckoo from protocol files:
./run [source files]
Generated mocks go to ShopHelpTests/GeneratedMocks.swift.
Testing Patterns
ViewModel Tests
@MainActor
final class MyViewModelTests: XCTestCase {
private var sut: MyViewModel!
private var mockUseCase: MockMyUseCaseProtocol!
override func setUp() {
super.setUp()
mockUseCase = MockMyUseCaseProtocol()
let dependencies = MyDependencies(useCase: mockUseCase)
sut = MyViewModel(dependencies: dependencies)
}
func test_loadData_success_stateIsLoaded() async {
stub(mockUseCase) { stub in
when(stub.execute()).thenReturn(mockData)
}
await sut.loadData()
XCTAssertEqual(sut.state, .loaded(mockData))
}
func test_loadData_failure_stateIsError() async {
stub(mockUseCase) { stub in
when(stub.execute()).thenThrow(TestError.generic)
}
await sut.loadData()
XCTAssertEqual(sut.state, .error)
XCTAssertNotNil(sut.errorMessage)
}
}
UseCase Tests
@MainActor
final class GetProductUseCaseTests: XCTestCase {
private var sut: GetProductUseCase!
private var mockRepository: MockProductRepositoryProtocol!
override func setUp() {
super.setUp()
mockRepository = MockProductRepositoryProtocol()
sut = GetProductUseCase(repository: mockRepository)
}
func test_execute_validId_returnsProduct() async throws {
stub(mockRepository) { stub in
when(stub.fetchProduct(id: any())).thenReturn(mockProduct)
}
let result = try await sut.execute(productId: "123")
XCTAssertEqual(result.id, "123")
}
}
Running Tests
make unit-test
make test
Single test class:
xcodebuild test \
-project ShopHelp.xcodeproj \
-scheme ShopHelp \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-only-testing:ShopHelpTests/TestClassName \
CODE_SIGNING_ALLOWED=NO
Checklist
Reference
See ShopHelpTests/ for existing test examples.
See docs/SETUP.md for test commands and mock generation.