ios-testing
SKILL.md
iOS Testing
Comprehensive testing strategies for robust iOS applications.
Testing Pyramid
/\
/ \ UI Tests (10%)
/----\ Integration Tests (20%)
/ \
/--------\ Unit Tests (70%)
/ \
Unit Testing
Setup with XCTest
import XCTest
@testable import MyApp
final class UserServiceTests: XCTestCase {
var sut: UserService! // System Under Test
var mockNetworkService: MockNetworkService!
override func setUpWithError() throws {
try super.setUpWithError()
mockNetworkService = MockNetworkService()
sut = UserService(networkService: mockNetworkService)
}
override func tearDownWithError() throws {
sut = nil
mockNetworkService = nil
try super.tearDownWithError()
}
// MARK: - Tests
func test_fetchUser_success() async throws {
// Given
let expectedUser = User(id: "1", name: "John")
mockNetworkService.result = .success(expectedUser)
// When
let user = try await sut.fetchUser(id: "1")
// Then
XCTAssertEqual(user.id, "1")
XCTAssertEqual(user.name, "John")
}
func test_fetchUser_failure() async {
// Given
mockNetworkService.result = .failure(NetworkError.notFound)
// When/Then
do {
_ = try await sut.fetchUser(id: "1")
XCTFail("Expected error to be thrown")
} catch {
XCTAssertEqual(error as? NetworkError, .notFound)
}
}
}
Test Naming Conventions
// Pattern: test_[method]_[scenario]_[expectedResult]
func test_login_withValidCredentials_returnsUser() { }
func test_login_withInvalidPassword_throwsAuthError() { }
func test_calculateTotal_withEmptyCart_returnsZero() { }
func test_formatDate_withNilDate_returnsPlaceholder() { }
Async Testing
// Modern async/await
func test_asyncOperation() async throws {
let result = try await sut.fetchData()
XCTAssertFalse(result.isEmpty)
}
// With timeout
func test_asyncWithTimeout() async throws {
try await withTimeout(seconds: 5) {
let result = try await sut.slowOperation()
XCTAssertNotNil(result)
}
}
// Combine testing
func test_publisher() {
let expectation = expectation(description: "Publisher completes")
var receivedValues: [String] = []
sut.dataPublisher
.sink(
receiveCompletion: { _ in expectation.fulfill() },
receiveValue: { receivedValues.append($0) }
)
.store(in: &cancellables)
wait(for: [expectation], timeout: 2)
XCTAssertEqual(receivedValues, ["expected"])
}
Mocking & Dependency Injection
Protocol-Based Mocking
// Production code
protocol UserRepositoryProtocol {
func getUser(id: String) async throws -> User
func saveUser(_ user: User) async throws
}
// Mock for testing
class MockUserRepository: UserRepositoryProtocol {
var getUserResult: Result<User, Error>!
var saveUserCalled = false
var savedUser: User?
func getUser(id: String) async throws -> User {
switch getUserResult! {
case .success(let user): return user
case .failure(let error): throw error
}
}
func saveUser(_ user: User) async throws {
saveUserCalled = true
savedUser = user
}
}
Dependency Injection
// Constructor injection (preferred)
class UserViewModel {
private let repository: UserRepositoryProtocol
init(repository: UserRepositoryProtocol = UserRepository()) {
self.repository = repository
}
}
// In tests
func test_example() {
let mockRepo = MockUserRepository()
let viewModel = UserViewModel(repository: mockRepo)
// Test with mock
}
UI Testing
Basic UI Test
import XCTest
final class LoginUITests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
try super.setUpWithError()
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments = ["--uitesting"]
app.launch()
}
func test_login_withValidCredentials_showsHomeScreen() {
// Given
let emailField = app.textFields["email-field"]
let passwordField = app.secureTextFields["password-field"]
let loginButton = app.buttons["login-button"]
// When
emailField.tap()
emailField.typeText("test@example.com")
passwordField.tap()
passwordField.typeText("password123")
loginButton.tap()
// Then
XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 5))
}
}
Accessibility Identifiers
// In production code
TextField("Email", text: $email)
.accessibilityIdentifier("email-field")
Button("Login") { }
.accessibilityIdentifier("login-button")
Page Object Pattern
// LoginPage.swift
struct LoginPage {
let app: XCUIApplication
var emailField: XCUIElement { app.textFields["email-field"] }
var passwordField: XCUIElement { app.secureTextFields["password-field"] }
var loginButton: XCUIElement { app.buttons["login-button"] }
var errorMessage: XCUIElement { app.staticTexts["error-message"] }
func login(email: String, password: String) {
emailField.tap()
emailField.typeText(email)
passwordField.tap()
passwordField.typeText(password)
loginButton.tap()
}
}
// In test
func test_login() {
let loginPage = LoginPage(app: app)
loginPage.login(email: "test@example.com", password: "password")
XCTAssertTrue(app.staticTexts["Welcome"].exists)
}
Snapshot Testing
Using swift-snapshot-testing
import SnapshotTesting
import SwiftUI
import XCTest
@testable import MyApp
final class HomeViewSnapshotTests: XCTestCase {
func test_homeView_lightMode() {
let view = HomeView()
.preferredColorScheme(.light)
assertSnapshot(
matching: view,
as: .image(layout: .device(config: .iPhone13Pro))
)
}
func test_homeView_darkMode() {
let view = HomeView()
.preferredColorScheme(.dark)
assertSnapshot(
matching: view,
as: .image(layout: .device(config: .iPhone13Pro))
)
}
func test_homeView_dynamicTypeXXL() {
let view = HomeView()
.environment(\.sizeCategory, .accessibilityExtraExtraLarge)
assertSnapshot(
matching: view,
as: .image(layout: .device(config: .iPhone13Pro))
)
}
}
Device Configurations
// Test across devices
func test_responsiveLayout() {
let view = MyView()
assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhoneSe)))
assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhone13Pro)))
assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhone13ProMax)))
assertSnapshot(matching: view, as: .image(layout: .device(config: .iPadPro11)))
}
Test Coverage
Enable Code Coverage
1. Edit Scheme → Test → Options
2. Check "Gather coverage for:"
3. Select targets
# View coverage
Product → Test → Show Test Report → Coverage tab
Coverage Targets
Recommended minimums:
- Models: 90%+
- ViewModels: 80%+
- Services: 80%+
- Utilities: 90%+
- Views: 60%+ (UI tests)
Overall target: 70-80%
Testing Best Practices
Test Isolation
// Each test should be independent
// Use setUp/tearDown for fresh state
// Never rely on test execution order
override func setUp() {
// Create fresh instance
sut = ViewModel()
}
override func tearDown() {
// Clean up
sut = nil
}
Test Data Builders
// UserBuilder.swift
struct UserBuilder {
private var id = "default-id"
private var name = "Default User"
private var email = "default@example.com"
func withId(_ id: String) -> Self {
var copy = self
copy.id = id
return copy
}
func withName(_ name: String) -> Self {
var copy = self
copy.name = name
return copy
}
func build() -> User {
User(id: id, name: name, email: email)
}
}
// Usage
let user = UserBuilder()
.withName("John")
.build()
Flakey Test Prevention
// Avoid:
- sleep() or fixed delays
- Real network calls
- File system dependencies
- Date/time dependencies
// Use instead:
- Proper async/await
- Mocked services
- In-memory storage
- Injected date providers
Running Tests
Command Line
# Run all tests
xcodebuild test \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15'
# Run specific test
xcodebuild test \
-only-testing:MyAppTests/UserServiceTests/test_fetchUser_success \
...
# With coverage
xcodebuild test \
-enableCodeCoverage YES \
...
Xcode Shortcuts
Cmd + U Run all tests
Ctrl + Opt + Cmd + U Run test under cursor
Ctrl + Opt + Cmd + G Re-run last test
Test Organization
Tests/
├── UnitTests/
│ ├── Models/
│ │ └── UserTests.swift
│ ├── ViewModels/
│ │ └── HomeViewModelTests.swift
│ ├── Services/
│ │ └── UserServiceTests.swift
│ └── Mocks/
│ ├── MockUserRepository.swift
│ └── MockNetworkService.swift
├── IntegrationTests/
│ └── APIIntegrationTests.swift
├── SnapshotTests/
│ └── HomeViewSnapshotTests.swift
└── UITests/
├── Pages/
│ └── LoginPage.swift
└── Flows/
└── LoginFlowTests.swift
Resources
See references/testing-patterns.md for advanced patterns. See references/tdd-guide.md for TDD workflow.
Weekly Installs
2
Repository
jx1100370217/my…w-skillsGitHub Stars
2
First Seen
14 days ago
Security Audits
Installed on
opencode2
gemini-cli2
claude-code2
github-copilot2
codex2
kimi-cli2