bookworm-smart-assistant/skills/swift-expert/references/testing-patterns.md

400 lines
9.2 KiB
Markdown

# Testing Patterns
## XCTest Basics
```swift
import XCTest
@testable import MyApp
final class UserTests: XCTestCase {
var sut: UserManager!
override func setUp() {
super.setUp()
sut = UserManager()
}
override func tearDown() {
sut = nil
super.tearDown()
}
func testUserCreation() {
// Given
let name = "John Doe"
let email = "john@example.com"
// When
let user = sut.createUser(name: name, email: email)
// Then
XCTAssertEqual(user.name, name)
XCTAssertEqual(user.email, email)
XCTAssertNotNil(user.id)
}
func testValidation() throws {
// Unwrapping optionals in tests
let user = try XCTUnwrap(sut.findUser(id: 123))
XCTAssertEqual(user.name, "Test User")
}
}
```
## Async Testing
```swift
final class AsyncTests: XCTestCase {
func testAsyncFunction() async throws {
// Test async/await code directly
let result = try await fetchData()
XCTAssertEqual(result.count, 10)
}
func testAsyncSequence() async throws {
var results: [Int] = []
for try await value in numberStream() {
results.append(value)
if results.count >= 5 {
break
}
}
XCTAssertEqual(results.count, 5)
}
func testWithTimeout() async throws {
// Test with timeout
try await withTimeout(seconds: 5) {
try await longRunningOperation()
}
}
func testConcurrentOperations() async throws {
async let result1 = fetchData(id: 1)
async let result2 = fetchData(id: 2)
let (data1, data2) = try await (result1, result2)
XCTAssertNotNil(data1)
XCTAssertNotNil(data2)
}
}
// Helper for timeout
func withTimeout<T>(
seconds: TimeInterval,
operation: @escaping () async throws -> T
) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask {
try await operation()
}
group.addTask {
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
throw TimeoutError()
}
let result = try await group.next()!
group.cancelAll()
return result
}
}
```
## Mocking
```swift
// Protocol for dependency injection
protocol DataService {
func fetch(id: Int) async throws -> Data
func save(_ data: Data) async throws
}
// Production implementation
class APIDataService: DataService {
func fetch(id: Int) async throws -> Data {
// Real API call
}
func save(_ data: Data) async throws {
// Real save operation
}
}
// Mock for testing
class MockDataService: DataService {
var fetchCalled = false
var fetchID: Int?
var fetchResult: Data?
var fetchError: Error?
var saveCalled = false
var savedData: Data?
var saveError: Error?
func fetch(id: Int) async throws -> Data {
fetchCalled = true
fetchID = id
if let error = fetchError {
throw error
}
return fetchResult ?? Data()
}
func save(_ data: Data) async throws {
saveCalled = true
savedData = data
if let error = saveError {
throw error
}
}
}
// Using mock in tests
final class DataManagerTests: XCTestCase {
func testDataFetch() async throws {
// Given
let mockService = MockDataService()
mockService.fetchResult = "test data".data(using: .utf8)
let manager = DataManager(service: mockService)
// When
let result = try await manager.loadData(id: 123)
// Then
XCTAssertTrue(mockService.fetchCalled)
XCTAssertEqual(mockService.fetchID, 123)
XCTAssertNotNil(result)
}
}
```
## Test Doubles
```swift
// Spy - records interactions
class SpyDelegate: UserManagerDelegate {
private(set) var didUpdateUserCalled = false
private(set) var updatedUser: User?
private(set) var callCount = 0
func didUpdateUser(_ user: User) {
didUpdateUserCalled = true
updatedUser = user
callCount += 1
}
}
// Stub - provides predetermined responses
class StubNetworkService: NetworkService {
var stubbedResponse: Result<Data, Error> = .success(Data())
func fetch(url: URL) async throws -> Data {
try stubbedResponse.get()
}
}
// Fake - working implementation with shortcuts
class FakeDatabase: Database {
private var storage: [String: Data] = [:]
func save(key: String, value: Data) {
storage[key] = value
}
func load(key: String) -> Data? {
storage[key]
}
func clear() {
storage.removeAll()
}
}
```
## Performance Testing
```swift
final class PerformanceTests: XCTestCase {
func testSortingPerformance() {
let numbers = (0..<10000).shuffled()
measure {
_ = numbers.sorted()
}
}
func testCustomMetrics() {
let metrics: [XCTMetric] = [
XCTClockMetric(),
XCTCPUMetric(),
XCTMemoryMetric(),
XCTStorageMetric()
]
let options = XCTMeasureOptions()
options.iterationCount = 10
measure(metrics: metrics, options: options) {
performExpensiveOperation()
}
}
}
```
## UI Testing
```swift
final class AppUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
func testLoginFlow() {
// Test UI interactions
let emailField = app.textFields["Email"]
emailField.tap()
emailField.typeText("test@example.com")
let passwordField = app.secureTextFields["Password"]
passwordField.tap()
passwordField.typeText("password123")
app.buttons["Login"].tap()
// Verify navigation
XCTAssertTrue(app.navigationBars["Dashboard"].exists)
}
func testButtonEnabled() {
let button = app.buttons["Submit"]
XCTAssertFalse(button.isEnabled)
app.textFields["Username"].tap()
app.textFields["Username"].typeText("testuser")
XCTAssertTrue(button.isEnabled)
}
}
```
## Testing Actors
```swift
final class ActorTests: XCTestCase {
func testActorIsolation() async throws {
actor Counter {
private var value = 0
func increment() -> Int {
value += 1
return value
}
func reset() {
value = 0
}
}
let counter = Counter()
// Test concurrent access
await withTaskGroup(of: Int.self) { group in
for _ in 0..<100 {
group.addTask {
await counter.increment()
}
}
}
let finalValue = await counter.increment()
XCTAssertEqual(finalValue, 101)
}
}
```
## Snapshot Testing
```swift
import SnapshotTesting
final class ViewSnapshotTests: XCTestCase {
func testButtonAppearance() {
let button = UIButton()
button.setTitle("Tap Me", for: .normal)
button.backgroundColor = .blue
button.frame = CGRect(x: 0, y: 0, width: 200, height: 50)
assertSnapshot(matching: button, as: .image)
}
func testViewControllerLayout() {
let vc = MyViewController()
assertSnapshot(matching: vc, as: .image(on: .iPhone13))
}
func testDarkMode() {
let view = MyView()
assertSnapshot(matching: view, as: .image(traits: .init(userInterfaceStyle: .dark)))
}
}
```
## Test Organization
```swift
// MARK: - Test Cases
extension UserManagerTests {
// MARK: Creation Tests
func testUserCreation() { }
func testUserCreationWithInvalidData() { }
// MARK: Validation Tests
func testEmailValidation() { }
func testPasswordValidation() { }
// MARK: Persistence Tests
func testUserSave() { }
func testUserLoad() { }
}
// MARK: - Test Helpers
extension UserManagerTests {
func makeTestUser() -> User {
User(name: "Test", email: "test@example.com")
}
func setupMockData() {
// Common test setup
}
}
```
## Best Practices
- Use `@testable import` to test internal types
- One assertion concept per test (can have multiple XCTAssert calls)
- Use Given-When-Then pattern for clarity
- Name tests descriptively: `test_methodName_condition_expectedResult`
- Use setUp/tearDown for common test setup
- Prefer dependency injection for testability
- Use protocols to enable mocking
- Test edge cases and error conditions
- Use async/await for testing async code
- Measure performance with XCTest metrics
- Use UI testing for critical user flows
- Mock external dependencies
- Keep tests fast and independent
- Use test doubles appropriately (mock, stub, spy, fake)