iOS Testing Expert
SKILL.md
iOS Testing Expert
Comprehensive testing patterns for iOS apps.
Testing Pyramid for iOS
/\ E2E (XCUITest) - 10%
/ \ UI Tests, full flows
/----\
/ \ Integration - 20%
/ \ ViewModels + Services
/----------\
/ \ Unit - 70%
\ Models, Utils, Pure Logic
XCTest Basics
Unit Test
import XCTest
@testable import App
final class UserTests: XCTestCase {
func testUserFullName() {
let user = User(firstName: "John", lastName: "Doe")
XCTAssertEqual(user.fullName, "John Doe")
}
func testUserValidation() throws {
let user = User(email: "invalid")
XCTAssertThrowsError(try user.validate())
}
}
Async Testing
func testAsyncFetch() async throws {
let service = UserService()
let user = try await service.fetch(id: "123")
XCTAssertEqual(user.id, "123")
}
// Or with expectations
func testAsyncWithExpectation() {
let expectation = expectation(description: "fetch")
service.fetch(id: "123") { result in
XCTAssertNotNil(result)
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
}
XCUITest (UI Tests)
Basic UI Test
import XCUITest
final class LoginUITests: XCTestCase {
let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
app.launch()
}
func testLogin() throws {
app.textFields["email"].tap()
app.textFields["email"].typeText("test@example.com")
app.secureTextFields["password"].tap()
app.secureTextFields["password"].typeText("password123")
app.buttons["Login"].tap()
XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 5))
}
}
Accessibility Identifiers
// In your SwiftUI View
TextField("Email", text: $email)
.accessibilityIdentifier("email")
// In UI Test
app.textFields["email"].tap()
Quick/Nimble (BDD Style)
Setup
// Package.swift
.package(url: "https://github.com/Quick/Quick.git", from: "7.0.0"),
.package(url: "https://github.com/Quick/Nimble.git", from: "13.0.0"),
BDD Test
import Quick
import Nimble
@testable import App
final class UserSpec: QuickSpec {
override class func spec() {
describe("User") {
var user: User!
beforeEach {
user = User(firstName: "John", lastName: "Doe")
}
context("when valid") {
it("has a full name") {
expect(user.fullName).to(equal("John Doe"))
}
it("can be validated") {
expect { try user.validate() }.toNot(throwError())
}
}
context("when email is invalid") {
beforeEach {
user.email = "invalid"
}
it("throws validation error") {
expect { try user.validate() }.to(throwError())
}
}
}
}
}
Mocking with Protocols
Protocol-Based DI
protocol UserServiceProtocol {
func fetch(id: String) async throws -> User
}
// Real implementation
class UserService: UserServiceProtocol {
func fetch(id: String) async throws -> User {
// Network call
}
}
// Mock for testing
class MockUserService: UserServiceProtocol {
var mockUser: User?
var shouldFail = false
func fetch(id: String) async throws -> User {
if shouldFail { throw APIError.failed }
return mockUser ?? User(id: id)
}
}
Testing with Mock
func testViewModelLoadsUser() async {
let mockService = MockUserService()
mockService.mockUser = User(id: "1", name: "Test")
let viewModel = UserViewModel(service: mockService)
await viewModel.load()
XCTAssertEqual(viewModel.user?.name, "Test")
}
Snapshot Testing
swift-snapshot-testing
import SnapshotTesting
import SwiftUI
func testUserProfileView() {
let view = UserProfileView(user: .mock)
assertSnapshot(
of: view,
as: .image(layout: .device(config: .iPhone13))
)
}
Test Coverage
Enable Coverage
xcodebuild test \
-scheme App \
-enableCodeCoverage YES \
-resultBundlePath TestResults.xcresult
View Report
xcrun xccov view --report TestResults.xcresult
Use when: Writing iOS tests, setting up testing infrastructure, mocking