Refactor high-complexity React components in Dify frontend. Use when `pnpm analyze-component...
npx skills add eovidiu/agents-skills --skill "ios-tdd-expert"
Install specific skill from multi-skill repository
# Description
Pragmatic Test-Driven Development skill for iOS engineers building native applications with Swift. Focus on shipping better code faster through behavioral testing, not dogma. Covers XCTest, Swift Testing framework (@Test, #expect), XCUITest for touch-based iOS interfaces (tap, swipe, scroll), protocol-based mocking, MVVM testability, async/await patterns, Combine testing, snapshot testing across device sizes, and performAccessibilityAudit (iOS 17+). Use when writing tests before implementation, testing ViewModels, SwiftUI views, UIKit controllers, services, navigation, or setting up testing infrastructure. Emphasizes testing what matters and skipping what doesn't.
# SKILL.md
name: ios-tdd-expert
description: Pragmatic Test-Driven Development skill for iOS engineers building native applications with Swift. Focus on shipping better code faster through behavioral testing, not dogma. Covers XCTest, Swift Testing framework (@Test, #expect), XCUITest for touch-based iOS interfaces (tap, swipe, scroll), protocol-based mocking, MVVM testability, async/await patterns, Combine testing, snapshot testing across device sizes, and performAccessibilityAudit (iOS 17+). Use when writing tests before implementation, testing ViewModels, SwiftUI views, UIKit controllers, services, navigation, or setting up testing infrastructure. Emphasizes testing what matters and skipping what doesn't.
Pragmatic TDD for iOS
Overview
Pragmatic iOS TDD Expert provides senior-level guidance for iOS engineers who write tests before implementation — not because it's dogma, but because it genuinely ships better code faster. This skill focuses on behavior-driven testing using XCTest and Swift Testing, realistic dependency mocking through protocols, and a ViewModel-centric integration approach that maximizes confidence while minimizing brittleness.
Use this skill when building or testing iOS applications with TDD, setting up testing infrastructure, reviewing code for test quality, or needing guidance on what to test (and what to skip).
Core Philosophy
Test to Ship Fast, Not to Reach 100%
Write tests that provide value. Skip tests that don't.
Test what matters:
- ViewModel state transitions and logic
- Model validation and business rules
- Service layer with mocked dependencies
- Error handling and recovery
- Data transformations and formatters
- Async workflows and state machines
- Navigation logic and deep link resolution
- Critical user flows (UI tests, sparingly)
- Accessibility compliance (iOS 17+)
- Visual consistency across device sizes (snapshot tests)
Skip what doesn't:
- SwiftUI view body composition
- Apple framework internals
- Auto Layout constraints and styling
- Trivial computed properties
- UIKit/SwiftUI rendering details
Reference: references/testing-philosophy.md - Complete pragmatic TDD philosophy, when to test vs. skip, Red-Green-Refactor cycle, and coverage strategies.
The iOS Testing Pyramid
/\
/ \ UI Tests (5-10%) - Critical flows only (XCUITest, touch-based)
/____\
/ \ Integration Tests (60-70%) - Your sweet spot
/ \ ViewModel + Services + mocked dependencies
/__________\
Unit Tests (20-30%) - Pure functions + models
Focus on integration tests. This is where iOS app value lives.
The TDD Cycle
Red -> Green -> Refactor
1. RED: Write a Failing Test
// Using Swift Testing
@Test func calculatesShippingCost() {
let order = Order(items: [Item(weight: 2.5)], destination: .domestic)
#expect(order.shippingCost == 7.50)
}
// Compilation error - shippingCost doesn't exist yet
2. GREEN: Make It Pass (Minimal Code)
extension Order {
var shippingCost: Double {
let totalWeight = items.reduce(0) { $0 + $1.weight }
return destination == .domestic ? totalWeight * 3.0 : totalWeight * 8.0
}
}
// Test passes
3. REFACTOR: Clean Up (Tests Still Pass)
extension Order {
var shippingCost: Double {
let totalWeight = totalItemWeight
return shippingRate(for: destination) * totalWeight
}
private var totalItemWeight: Double {
items.reduce(0) { $0 + $1.weight }
}
private func shippingRate(for destination: Destination) -> Double {
switch destination {
case .domestic: return 3.0
case .international: return 8.0
}
}
}
// Tests still pass - better code, same behavior
Quick Start
1. Set Up Testing Infrastructure
Use the setup script:
bash scripts/setup-testing.sh
Or manually add a test target in Xcode:
- File -> New -> Target -> iOS Unit Testing Bundle
- File -> New -> Target -> iOS UI Testing Bundle
2. Choose Your Testing Framework
XCTest (mature, Xcode-integrated):
import XCTest
@testable import MyApp
final class UserViewModelTests: XCTestCase {
func testLoadsUsers() async {
// ...
}
}
Swift Testing (modern, expressive):
import Testing
@testable import MyApp
struct UserViewModelTests {
@Test func loadsUsers() async {
// ...
}
}
Reference: references/xctest-reference.md - Complete comparison of XCTest vs Swift Testing, migration guide, and when to use each.
3. Set Up Dependency Injection
Define protocols for external dependencies:
protocol UserServiceProtocol: Sendable {
func fetchUsers() async throws -> [User]
func createUser(_ user: User) async throws -> User
}
Create mock implementations for tests:
final class MockUserService: UserServiceProtocol {
var usersToReturn: [User] = []
var errorToThrow: Error?
func fetchUsers() async throws -> [User] {
if let error = errorToThrow { throw error }
return usersToReturn
}
func createUser(_ user: User) async throws -> User {
if let error = errorToThrow { throw error }
return user
}
}
4. Run Tests from CLI
# iOS Simulator tests
xcodebuild test \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-resultBundlePath TestResults.xcresult
# With code coverage
xcodebuild test \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-enableCodeCoverage YES
# Extract coverage report
xcrun xccov view --report TestResults.xcresult
# SPM tests (for framework/library projects)
swift test --parallel
# SPM with coverage
swift test --enable-code-coverage
5. Start Testing!
Use templates for common patterns:
- assets/templates/ViewModelTests.swift - ViewModel testing
- assets/templates/ServiceTests.swift - Service/networking layer
- assets/templates/NavigationTests.swift - Navigation and deep links
- assets/templates/XCUITestTemplate.swift - Touch-based UI tests
- assets/templates/SnapshotTests.swift - Visual regression across devices
- assets/templates/AccessibilityTests.swift - Accessibility audits (iOS 17+)
- assets/templates/TestHelpers.swift - Common utilities
When to Use This Skill
Trigger this skill when:
- Writing tests before implementing iOS features (TDD)
- Testing ViewModels and their state transitions
- Testing service layers with mocked dependencies
- Writing XCUITest UI tests for iOS (touch: tap, swipe, scroll)
- Testing async/await and Combine code
- Testing navigation logic and deep link resolution
- Snapshot testing across iPhone SE, iPhone 16, iPad
- Running accessibility audits with performAccessibilityAudit (iOS 17+)
- Setting up testing infrastructure for iOS projects
- Reviewing code for testability
- Deciding what to test vs. skip
- Debugging flaky or failing tests
Testing Patterns by Use Case
1. ViewModel Testing (Integration Layer)
Test logic through ViewModels - this is your primary testing surface.
@MainActor
final class UserListViewModelTests: XCTestCase {
var mockService: MockUserService!
var viewModel: UserListViewModel!
override func setUp() {
mockService = MockUserService()
viewModel = UserListViewModel(service: mockService)
}
func testLoadsAndDisplaysUsers() async {
mockService.usersToReturn = [
User(id: "1", name: "Alice"),
User(id: "2", name: "Bob")
]
await viewModel.loadUsers()
XCTAssertEqual(viewModel.users.count, 2)
XCTAssertEqual(viewModel.users.first?.name, "Alice")
XCTAssertFalse(viewModel.isLoading)
XCTAssertNil(viewModel.errorMessage)
}
func testShowsErrorOnFailure() async {
mockService.errorToThrow = ServiceError.networkUnavailable
await viewModel.loadUsers()
XCTAssertTrue(viewModel.users.isEmpty)
XCTAssertNotNil(viewModel.errorMessage)
XCTAssertFalse(viewModel.isLoading)
}
}
Reference: references/xctest-reference.md - XCTest and Swift Testing patterns for ViewModels.
Template: assets/templates/ViewModelTests.swift
2. Service Layer Testing
Mock the network, test real service logic.
final class UserServiceTests: XCTestCase {
func testFetchUsersDecodesResponse() async throws {
let mockSession = MockURLSession(
data: usersJSON.data(using: .utf8)!,
response: HTTPURLResponse(statusCode: 200)
)
let service = UserService(session: mockSession)
let users = try await service.fetchUsers()
XCTAssertEqual(users.count, 2)
XCTAssertEqual(users.first?.name, "Alice")
}
func testFetchUsersThrowsOnServerError() async {
let mockSession = MockURLSession(
data: Data(),
response: HTTPURLResponse(statusCode: 500)
)
let service = UserService(session: mockSession)
do {
_ = try await service.fetchUsers()
XCTFail("Expected error")
} catch {
XCTAssertTrue(error is ServiceError)
}
}
}
Reference: references/mocking-patterns.md - Protocol mocking, URLProtocol stubs, and dependency injection patterns.
Template: assets/templates/ServiceTests.swift
3. XCUITest for iOS (Touch-Based)
Test critical user flows through the actual UI with touch gestures.
final class LoginUITests: XCTestCase {
let app = XCUIApplication()
override func setUp() {
continueAfterFailure = false
app.launchArguments = ["--ui-testing"]
app.launch()
}
func testLoginFlowShowsDashboard() {
let emailField = app.textFields["Email"]
emailField.tap()
emailField.typeText("[email protected]")
let passwordField = app.secureTextFields["Password"]
passwordField.tap()
passwordField.typeText("password123")
app.buttons["Sign In"].tap()
XCTAssertTrue(
app.staticTexts["Welcome, Alice"].waitForExistence(timeout: 5)
)
}
func testPullToRefreshUpdatesContent() {
let firstCell = app.cells.firstMatch
XCTAssertTrue(firstCell.waitForExistence(timeout: 5))
let start = firstCell.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
let end = firstCell.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 5.0))
start.press(forDuration: 0, thenDragTo: end)
// Assert refresh indicator appeared and content updated
}
}
Reference: references/xcuitest-guide.md - XCUITest patterns for iOS touch interactions.
Template: assets/templates/XCUITestTemplate.swift
4. Navigation Testing
Test navigation logic without UI.
@MainActor
func testDeepLinkNavigatesToProfile() {
let router = AppRouter()
let url = URL(string: "myapp://profile/123")!
router.handle(deepLink: url)
XCTAssertEqual(router.path.count, 1)
if case .profile(let id) = router.path.last {
XCTAssertEqual(id, "123")
} else {
XCTFail("Expected profile route")
}
}
Template: assets/templates/NavigationTests.swift
5. Snapshot Testing (Multi-Device)
Test visual consistency across iOS device sizes.
func testProfileViewiPhoneSE() {
let view = ProfileView(user: .preview)
assertSnapshot(
of: view,
as: .image(layout: .device(config: .iPhoneSe)),
record: false
)
}
Reference: references/snapshot-testing.md - swift-snapshot-testing for iOS devices.
Template: assets/templates/SnapshotTests.swift
6. Accessibility Testing (iOS 17+)
Automated accessibility audits.
func testHomeScreenAccessibility() throws {
let app = XCUIApplication()
app.launch()
try app.performAccessibilityAudit()
}
Reference: references/accessibility-testing.md - performAccessibilityAudit guide.
Template: assets/templates/AccessibilityTests.swift
Key Principles
1. Test Behavior, Not Implementation
// BAD - Tests internal state
XCTAssertTrue(viewModel.internalFlag)
// GOOD - Tests observable behavior
XCTAssertEqual(viewModel.displayTitle, "2 items in cart")
2. Mock Externally, Integrate Internally
// GOOD - Mock external dependency via protocol
let mockService = MockUserService(users: testUsers)
let viewModel = UserListViewModel(service: mockService)
// BAD - Mock your own ViewModel
let mockViewModel = MockUserListViewModel() // Don't do this
3. Use Accessibility Identifiers, Not View Hierarchy
// GOOD - Stable identifier
app.buttons["submitOrder"]
// BAD - Fragile hierarchy traversal
app.scrollViews.firstMatch.otherElements.buttons.element(boundBy: 2)
4. Use async/await, Not Expectations (When Possible)
// GOOD - Direct async
@MainActor
func testLoadsData() async {
await viewModel.loadData()
XCTAssertEqual(viewModel.items.count, 3)
}
// OK - XCTestExpectation for callback-based APIs
func testLegacyCallback() {
let expectation = expectation(description: "Callback fired")
legacyAPI.fetch { result in
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
}
5. Touch-Based Interactions for iOS
// iOS uses tap, swipe, scroll — NOT click
app.buttons["Submit"].tap()
app.cells.firstMatch.swipeLeft()
app.tables.firstMatch.swipeUp()
Anti-Patterns to Avoid
Testing SwiftUI View Body
// BAD - Inspecting view hierarchy
let view = ContentView()
// Trying to assert on body structure
// GOOD - Test the ViewModel that drives the view
let vm = ContentViewModel()
await vm.load()
XCTAssertEqual(vm.title, "Dashboard")
Testing Apple Frameworks
// BAD
func testNavigationStackPushesView() { /* ... */ }
func testStatePropertyWrapperUpdates() { /* ... */ }
func testCombinePublisherEmits() { /* ... */ }
// GOOD - Test YOUR logic
func testViewModelTransitionsToDetailState() async { /* ... */ }
Using Sleep Instead of Expectations
// BAD
Thread.sleep(forTimeInterval: 2.0)
// GOOD
await fulfillment(of: [expectation], timeout: 5)
// GOOD
await viewModel.loadData()
Giant Test Methods
// BAD - 200 lines testing everything
func testCompleteUserJourney() { /* ... */ }
// GOOD - Focused tests
func testUserCanLogIn() async { /* ... */ }
func testUserCanBrowseProducts() async { /* ... */ }
func testUserCanCheckout() async { /* ... */ }
Decision Tree: What to Test
Is it a pure function or model logic?
|-- YES -> Unit test
|-- NO |
Does it involve ViewModel state transitions or service integration?
|-- YES -> Integration test (ViewModel + mocked services)
|-- NO |
Is it a critical user flow spanning multiple screens?
|-- YES -> XCUITest UI test (touch-based)
|-- NO |
Does it need visual consistency across device sizes?
|-- YES -> Snapshot test (iPhone SE, iPhone 16, iPad)
|-- NO |
Does it need accessibility compliance?
|-- YES -> performAccessibilityAudit (iOS 17+)
|-- NO -> Probably don't test it
Speed Matters
Fast tests = fast feedback = fast iteration.
Optimize for speed:
- Unit tests: <50ms each
- ViewModel integration tests: <200ms each
- XCUITest: <10 seconds each
- Snapshot tests: <500ms each
Run in parallel:
# Xcode parallel testing
xcodebuild test \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-parallel-testing-enabled YES
# SPM parallel testing (default)
swift test --parallel
Resources
references/
Comprehensive documentation loaded as needed:
testing-philosophy.md- When to test, what to skip, Red-Green-Refactor, coverage strategiesxctest-reference.md- XCTest and Swift Testing framework patternsxcuitest-guide.md- XCUITest for iOS: touch gestures, queries, screenshotsmocking-patterns.md- Protocol mocking, Combine publishers, @Observable, URLProtocolsnapshot-testing.md- swift-snapshot-testing across device sizes and traitsaccessibility-testing.md- performAccessibilityAudit (iOS 17+), VoiceOver strategies
scripts/
Setup utilities:
setup-testing.sh- Set up test targets, verify simulators, smoke test
assets/templates/
Copy-paste templates for common scenarios:
ViewModelTests.swift- ViewModel testing with mocked servicesServiceTests.swift- Service/networking layer with URLProtocolNavigationTests.swift- Navigation logic and deep linksXCUITestTemplate.swift- Touch-based UI testsSnapshotTests.swift- Visual regression across device sizesAccessibilityTests.swift- Accessibility audit tests (iOS 17+)TestHelpers.swift- Common test utilities and factories
Summary
TDD is a tool for shipping better iOS apps faster.
- 70% integration tests - ViewModels + services with mocked dependencies
- 25% unit tests - Pure functions + model logic
- 5% UI tests - Critical flows with XCUITest (touch-based)
Test behavior, not implementation.
Mock externally, integrate internally.
Touch-first: tap, swipe, scroll — never click.
Speed is a feature.
Write tests that make you ship with confidence, not tests that make you scared to refactor.
# 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.