Refactor high-complexity React components in Dify frontend. Use when `pnpm analyze-component...
npx skills add jx1100370217/my-openclaw-skills --skill "ios-testing"
Install specific skill from multi-skill repository
# Description
Implement comprehensive testing strategies for iOS apps. Use when writing unit tests, UI tests, integration tests, snapshot tests, setting up test coverage, mocking dependencies, or implementing TDD. Part of the idea-to-App-Store workflow between development and CI/CD.
# SKILL.md
name: ios-testing
description: Implement comprehensive testing strategies for iOS apps. Use when writing unit tests, UI tests, integration tests, snapshot tests, setting up test coverage, mocking dependencies, or implementing TDD. Part of the idea-to-App-Store workflow between development and CI/CD.
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("[email protected]")
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: "[email protected]", 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 = "[email protected]"
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.
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.