# Testing Unit tests, UI tests, snapshot tests, and testing patterns for iOS apps. ## Swift Testing (Xcode 16+) ### Basic Tests ```swift import Testing @testable import MyApp @Suite("Item Tests") struct ItemTests { @Test("Create item with name") func createItem() { let item = Item(name: "Test") #expect(item.name == "Test") #expect(item.isCompleted == false) } @Test("Toggle completion") func toggleCompletion() { var item = Item(name: "Test") item.isCompleted = true #expect(item.isCompleted == true) } } ``` ### Async Tests ```swift @Test("Fetch items from network") func fetchItems() async throws { let service = MockNetworkService() service.mockResult = [Item(name: "Test")] let viewModel = ItemListViewModel(networkService: service) await viewModel.load() #expect(viewModel.items.count == 1) #expect(viewModel.items[0].name == "Test") } @Test("Handle network error") func handleNetworkError() async { let service = MockNetworkService() service.mockError = NetworkError.noConnection let viewModel = ItemListViewModel(networkService: service) await viewModel.load() #expect(viewModel.items.isEmpty) #expect(viewModel.error != nil) } ``` ### Parameterized Tests ```swift @Test("Validate email", arguments: [ ("test@example.com", true), ("invalid", false), ("@example.com", false), ("test@", false) ]) func validateEmail(email: String, expected: Bool) { let isValid = EmailValidator.isValid(email) #expect(isValid == expected) } ``` ### Test Lifecycle ```swift @Suite("Database Tests") struct DatabaseTests { let database: TestDatabase init() async throws { database = try await TestDatabase.create() } @Test func insertItem() async throws { try await database.insert(Item(name: "Test")) let items = try await database.fetchAll() #expect(items.count == 1) } } ``` ## XCTest (Traditional) ### Basic XCTest ```swift import XCTest @testable import MyApp class ItemTests: XCTestCase { var sut: Item! override func setUp() { super.setUp() sut = Item(name: "Test") } override func tearDown() { sut = nil super.tearDown() } func testCreateItem() { XCTAssertEqual(sut.name, "Test") XCTAssertFalse(sut.isCompleted) } func testToggleCompletion() { sut.isCompleted = true XCTAssertTrue(sut.isCompleted) } } ``` ### Async XCTest ```swift func testFetchItems() async throws { let service = MockNetworkService() service.mockResult = [Item(name: "Test")] let viewModel = ItemListViewModel(networkService: service) await viewModel.load() XCTAssertEqual(viewModel.items.count, 1) } ``` ## Mocking ### Protocol-Based Mocks ```swift // Protocol protocol NetworkServiceProtocol { func fetch(_ endpoint: Endpoint) async throws -> T } // Mock class MockNetworkService: NetworkServiceProtocol { var mockResult: Any? var mockError: Error? var fetchCallCount = 0 func fetch(_ endpoint: Endpoint) async throws -> T { fetchCallCount += 1 if let error = mockError { throw error } guard let result = mockResult as? T else { fatalError("Mock result type mismatch") } return result } } ``` ### Testing with Mocks ```swift @Test func loadItemsCallsNetwork() async { let mock = MockNetworkService() mock.mockResult = [Item]() let viewModel = ItemListViewModel(networkService: mock) await viewModel.load() #expect(mock.fetchCallCount == 1) } ``` ## Testing SwiftUI Views ### View Tests with ViewInspector ```swift import ViewInspector @testable import MyApp @Test func itemRowDisplaysName() throws { let item = Item(name: "Test Item") let view = ItemRow(item: item) let text = try view.inspect().hStack().text(0).string() #expect(text == "Test Item") } ``` ### Testing View Models ```swift @Test func viewModelUpdatesOnSelection() async { let viewModel = ItemListViewModel() viewModel.items = [Item(name: "A"), Item(name: "B")] viewModel.select(viewModel.items[0]) #expect(viewModel.selectedItem?.name == "A") } ``` ## UI Testing ### Basic UI Test ```swift import XCTest class MyAppUITests: XCTestCase { let app = XCUIApplication() override func setUpWithError() throws { continueAfterFailure = false app.launchArguments = ["--uitesting"] app.launch() } func testAddItem() { // Tap add button app.buttons["Add"].tap() // Enter name let textField = app.textFields["Item name"] textField.tap() textField.typeText("New Item") // Save app.buttons["Save"].tap() // Verify XCTAssertTrue(app.staticTexts["New Item"].exists) } func testSwipeToDelete() { // Assume item exists let cell = app.cells["Item Row"].firstMatch // Swipe and delete cell.swipeLeft() app.buttons["Delete"].tap() // Verify XCTAssertFalse(cell.exists) } } ``` ### Accessibility Identifiers ```swift struct ItemRow: View { let item: Item var body: some View { HStack { Text(item.name) } .accessibilityIdentifier("Item Row") } } struct NewItemView: View { @State private var name = "" var body: some View { TextField("Item name", text: $name) .accessibilityIdentifier("Item name") } } ``` ### Launch Arguments for Testing ```swift @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .onAppear { if CommandLine.arguments.contains("--uitesting") { // Use mock data // Skip onboarding // Clear state } } } } } ``` ## Snapshot Testing Using swift-snapshot-testing: ```swift import SnapshotTesting import XCTest @testable import MyApp class SnapshotTests: XCTestCase { func testItemRow() { let item = Item(name: "Test", subtitle: "Subtitle") let view = ItemRow(item: item) .frame(width: 375) assertSnapshot(of: view, as: .image) } func testItemRowDarkMode() { let item = Item(name: "Test", subtitle: "Subtitle") let view = ItemRow(item: item) .frame(width: 375) .preferredColorScheme(.dark) assertSnapshot(of: view, as: .image, named: "dark") } func testItemRowLargeText() { let item = Item(name: "Test", subtitle: "Subtitle") let view = ItemRow(item: item) .frame(width: 375) .environment(\.sizeCategory, .accessibilityExtraLarge) assertSnapshot(of: view, as: .image, named: "large-text") } } ``` ## Testing SwiftData ```swift @Suite("SwiftData Tests") struct SwiftDataTests { @Test func insertAndFetch() async throws { // In-memory container for testing let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try ModelContainer(for: Item.self, configurations: config) let context = container.mainContext // Insert let item = Item(name: "Test") context.insert(item) try context.save() // Fetch let descriptor = FetchDescriptor() let items = try context.fetch(descriptor) #expect(items.count == 1) #expect(items[0].name == "Test") } } ``` ## Testing Network Calls ### Using URLProtocol ```swift class MockURLProtocol: URLProtocol { static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override class func canInit(with request: URLRequest) -> Bool { return true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } override func startLoading() { guard let handler = MockURLProtocol.requestHandler else { fatalError("Handler not set") } do { let (response, data) = try handler(request) client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } override func stopLoading() {} } @Test func fetchItemsReturnsData() async throws { // Configure mock let config = URLSessionConfiguration.ephemeral config.protocolClasses = [MockURLProtocol.self] let session = URLSession(configuration: config) let mockItems = [Item(name: "Test")] let mockData = try JSONEncoder().encode(mockItems) MockURLProtocol.requestHandler = { request in let response = HTTPURLResponse( url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil )! return (response, mockData) } // Test let service = NetworkService(session: session) let items: [Item] = try await service.fetch(.items) #expect(items.count == 1) } ``` ## Test Helpers ### Factory Methods ```swift extension Item { static func sample( name: String = "Sample", isCompleted: Bool = false, priority: Int = 0 ) -> Item { Item(name: name, isCompleted: isCompleted, priority: priority) } static var samples: [Item] { [ .sample(name: "First"), .sample(name: "Second", isCompleted: true), .sample(name: "Third", priority: 5) ] } } ``` ### Async Test Utilities ```swift func waitForCondition( timeout: TimeInterval = 1.0, condition: @escaping () -> Bool ) async throws { let start = Date() while !condition() { if Date().timeIntervalSince(start) > timeout { throw TestError.timeout } try await Task.sleep(nanoseconds: 10_000_000) // 10ms } } enum TestError: Error { case timeout } ``` ## Running Tests from CLI ```bash # Run all tests xcodebuild test \ -project MyApp.xcodeproj \ -scheme MyApp \ -destination 'platform=iOS Simulator,name=iPhone 16' # Run specific test xcodebuild test \ -project MyApp.xcodeproj \ -scheme MyApp \ -destination 'platform=iOS Simulator,name=iPhone 16' \ -only-testing:MyAppTests/ItemTests # With code coverage xcodebuild test \ -project MyApp.xcodeproj \ -scheme MyApp \ -destination 'platform=iOS Simulator,name=iPhone 16' \ -enableCodeCoverage YES \ -resultBundlePath TestResults.xcresult ``` ## Best Practices ### Test Naming ```swift // Describe what is being tested and expected outcome @Test func itemListViewModel_load_setsItemsFromNetwork() @Test func purchaseService_purchaseProduct_updatesEntitlements() ``` ### Arrange-Act-Assert ```swift @Test func toggleCompletion() { // Arrange var item = Item(name: "Test") // Act item.isCompleted.toggle() // Assert #expect(item.isCompleted == true) } ``` ### One Assertion Per Test Focus each test on a single behavior: ```swift // Good @Test func loadSetsItems() async { ... } @Test func loadSetsLoadingFalse() async { ... } @Test func loadClearsError() async { ... } // Avoid @Test func loadWorks() async { // Too many assertions } ```