eovidiu

macos-tdd-expert

2
0
# Install this skill:
npx skills add eovidiu/agents-skills --skill "macos-tdd-expert"

Install specific skill from multi-skill repository

# Description

Pragmatic Test-Driven Development skill for macOS engineers building native applications with Swift. Focus on shipping better code faster through behavioral testing, not dogma. Covers XCTest, Swift Testing framework, XCUITest for macOS, protocol-based mocking, MVVM testability, async/await patterns, and Combine testing. Use when writing tests before implementation, testing ViewModels, SwiftUI views, AppKit controllers, services, or setting up testing infrastructure. Emphasizes testing what matters and skipping what doesn't.

# SKILL.md


name: macos-tdd-expert
description: Pragmatic Test-Driven Development skill for macOS engineers building native applications with Swift. Focus on shipping better code faster through behavioral testing, not dogma. Covers XCTest, Swift Testing framework, XCUITest for macOS, protocol-based mocking, MVVM testability, async/await patterns, and Combine testing. Use when writing tests before implementation, testing ViewModels, SwiftUI views, AppKit controllers, services, or setting up testing infrastructure. Emphasizes testing what matters and skipping what doesn't.


Pragmatic TDD for macOS

Overview

Pragmatic macOS TDD Expert provides senior-level guidance for macOS 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 macOS 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
- Critical user flows (UI tests, sparingly)
- Accessibility compliance

Skip what doesn't:
- SwiftUI view body composition
- Apple framework internals
- Auto Layout constraints and styling
- Trivial computed properties
- AppKit/SwiftUI rendering details

Reference: references/philosophy.md - Complete pragmatic TDD philosophy, when to test vs. skip, Red-Green-Refactor cycle, and coverage strategies.

The macOS Testing Pyramid

     /\
    /  \     UI Tests (5-10%) - Critical flows only (XCUITest)
   /____\
  /      \   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 macOS app value lives.

Reference: references/testing-pyramid.md - Detailed breakdown of unit vs integration vs UI, when to use each layer, and practical distribution examples.

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 for SPM projects:

bash scripts/setup-testing.sh

Or manually add a test target in Xcode:
- File β†’ New β†’ Target β†’ macOS Unit 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-testing.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. Start Testing!

Use templates for common patterns:
- assets/templates/ViewModelTests.swift - ViewModel testing
- assets/templates/ViewTests.swift - SwiftUI view testing
- assets/templates/ServiceTests.swift - Service/networking layer
- assets/templates/AppKitTests.swift - AppKit controllers
- assets/templates/SnapshotTests.swift - Visual regression
- assets/templates/TestHelpers.swift - Common utilities

When to Use This Skill

Trigger this skill when:
- Writing tests before implementing macOS features (TDD)
- Testing ViewModels and their state transitions
- Testing service layers with mocked dependencies
- Writing XCUITest UI tests for macOS apps
- Testing async/await and Combine code
- Testing AppKit controllers and views
- Setting up testing infrastructure for macOS 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-testing.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-strategies.md - Protocol mocking, URLProtocol stubs, and dependency injection patterns.

Template: assets/templates/ServiceTests.swift

3. XCUITest for macOS

Test critical user flows through the actual UI.

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.click()
        emailField.typeText("[email protected]")

        let passwordField = app.secureTextFields["Password"]
        passwordField.click()
        passwordField.typeText("password123")

        app.buttons["Sign In"].click()

        XCTAssertTrue(
            app.staticTexts["Welcome, Alice"].waitForExistence(timeout: 5)
        )
    }
}

Reference: references/ui-testing.md - XCUITest patterns for macOS including menus, windows, sheets, and keyboard shortcuts.

Template: assets/templates/ViewTests.swift

4. Async/Await and Combine Testing

Test asynchronous code with structured concurrency.

@MainActor
func testDebounceSearch() async {
    let mockService = MockSearchService()
    let viewModel = SearchViewModel(service: mockService)

    viewModel.searchText = "Swi"
    viewModel.searchText = "Swif"
    viewModel.searchText = "Swift"

    // Wait for debounce
    try? await Task.sleep(for: .milliseconds(400))

    XCTAssertEqual(mockService.searchCallCount, 1)
    XCTAssertEqual(mockService.lastQuery, "Swift")
}

Template: assets/templates/ViewModelTests.swift

5. AppKit Controller Testing

Test NSViewControllers and NSWindowControllers.

final class PreferencesControllerTests: XCTestCase {
    func testDisplaysCurrentSettings() {
        let mockSettings = MockSettingsStore(theme: .dark, fontSize: 14)
        let controller = PreferencesViewController(settings: mockSettings)
        controller.loadView()
        controller.viewDidLoad()

        XCTAssertEqual(controller.themePopUp.titleOfSelectedItem, "Dark")
        XCTAssertEqual(controller.fontSizeSlider.integerValue, 14)
    }
}

Template: assets/templates/AppKitTests.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"]

// GOOD - Semantic label
app.buttons["Place Order"]

// BAD - Fragile hierarchy traversal
app.windows.firstMatch.groups.element(boundBy: 2).buttons.firstMatch

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
        // assert...
        expectation.fulfill()
    }
    wait(for: [expectation], timeout: 5)
}

5. Write Minimal Code to Pass

Let tests drive implementation incrementally.

// First test
@Test func emptyCartHasZeroTotal() {
    let cart = Cart(items: [])
    #expect(cart.total == 0)
}

// Minimal implementation
struct Cart {
    let items: [Item]
    var total: Double { 0 } // Good enough for now
}

// Next test drives real logic
@Test func cartSumsItemPrices() {
    let cart = Cart(items: [Item(price: 10), Item(price: 20)])
    #expect(cart.total == 30)
}

// Now implement
struct Cart {
    let items: [Item]
    var total: Double { items.reduce(0) { $0 + $1.price } }
}

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
try await Task.sleep(for: .milliseconds(100))
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 { /* ... */ }

Speed Matters

Fast tests = fast feedback = fast iteration.

Optimize for speed:
- Unit tests: <50ms each
- ViewModel integration tests: <200ms each
- XCUITest: <10 seconds each

Run in parallel:

# Xcode parallel testing
xcodebuild test -scheme MyApp -parallel-testing-enabled YES

# SPM parallel testing (default)
swift test --parallel

Resources

references/

Comprehensive documentation loaded as needed:

  • philosophy.md - When to test, what to skip, Red-Green-Refactor, coverage strategies
  • testing-pyramid.md - Unit vs integration vs UI distribution and examples
  • xctest-testing.md - XCTest and Swift Testing framework patterns
  • ui-testing.md - XCUITest for macOS: windows, menus, sheets, popovers
  • patterns.md - Common patterns, anti-patterns, best practices checklist
  • mocking-strategies.md - Protocol mocking, URLProtocol, dependency injection

scripts/

Setup utilities:

  • setup-testing.sh - Set up test targets and dependencies

assets/templates/

Copy-paste templates for common scenarios:

  • ViewModelTests.swift - ViewModel testing with mocked services
  • ViewTests.swift - SwiftUI view testing approaches
  • ServiceTests.swift - Service/networking layer with URLProtocol
  • AppKitTests.swift - AppKit NSViewController testing
  • SnapshotTests.swift - Visual regression with SwiftUI previews
  • TestHelpers.swift - Common test utilities and factories

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
└─ NO β†’ Probably don't test it

Getting Help

For specific topics:

  • Philosophy: When to test vs. skip β†’ references/philosophy.md
  • XCTest/Swift Testing: Framework patterns β†’ references/xctest-testing.md
  • UI Testing: XCUITest for macOS β†’ references/ui-testing.md
  • Mocking: Protocols and DI β†’ references/mocking-strategies.md
  • Test strategy: Unit vs integration vs UI β†’ references/testing-pyramid.md
  • Common patterns: Best practices β†’ references/patterns.md
  • Setup project: Run scripts/setup-testing.sh
  • Templates: Copy from assets/templates/

Summary

TDD is a tool for shipping better macOS apps faster.

  • 70% integration tests - ViewModels + services with mocked dependencies
  • 25% unit tests - Pure functions + model logic
  • 5% UI tests - Critical flows with XCUITest

Test behavior, not implementation.
Mock externally, integrate internally.
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.