Test-Driven Development patterns for macOS apps. Write tests first, implement minimal code to pass, refactor while keeping tests green. Covers SwiftData testing, network mocking, @Observable state testing, and UI testing patterns.
Test-Driven Development cycle for macOS apps:
1. **Write failing test** - Specify expected behavior
2. **Run test** - Verify RED (fails as expected)
3. **Implement** - Minimal code to pass
4. **Run test** - Verify GREEN (passes)
5. **Refactor** - Clean up while keeping green
6. **Run suite** - Ensure no regressions
Repeat for each feature. Keep tests running fast.
```
MyApp/
├── MyApp/
│ └── ... (production code)
└── MyAppTests/
├── ModelTests/
│ ├── ItemTests.swift
│ └── ItemStoreTests.swift
├── ServiceTests/
│ ├── NetworkServiceTests.swift
│ └── StorageServiceTests.swift
└── ViewModelTests/
└── AppStateTests.swift
```
Group tests by layer. One test file per production file/class.
SwiftData requires ModelContainer. Create in-memory container for tests:
```swift
@MainActor
class ItemTests: XCTestCase {
var container: ModelContainer!
var context: ModelContext!
override func setUp() async throws {
// In-memory container (doesn't persist)
let schema = Schema([Item.self, Tag.self])
let config = ModelConfiguration(isStoredInMemoryOnly: true)
container = try ModelContainer(for: schema, configurations: config)
context = ModelContext(container)
}
override func tearDown() {
container = nil
context = nil
}
func testCreateItem() throws {
let item = Item(name: "Test")
context.insert(item)
try context.save()
let fetched = try context.fetch(FetchDescriptor- ())
XCTAssertEqual(fetched.count, 1)
XCTAssertEqual(fetched.first?.name, "Test")
}
}
```
Critical: Test relationship behavior with in-memory container:
```swift
func testDeletingParentCascadesToChildren() throws {
let parent = Parent(name: "Parent")
let child1 = Child(name: "Child1")
let child2 = Child(name: "Child2")
child1.parent = parent
child2.parent = parent
context.insert(parent)
context.insert(child1)
context.insert(child2)
try context.save()
context.delete(parent)
try context.save()
let children = try context.fetch(FetchDescriptor())
XCTAssertEqual(children.count, 0) // Cascade delete worked
}
```
```swift
protocol NetworkSession {
func data(for request: URLRequest) async throws -> (Data, URLResponse)
}
extension URLSession: NetworkSession {}
class MockNetworkSession: NetworkSession {
var mockData: Data?
var mockResponse: URLResponse?
var mockError: Error?
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
if let error = mockError { throw error }
return (mockData ?? Data(), mockResponse ?? URLResponse())
}
}
// Test
func testFetchItems() async throws {
let json = """
[{"id": 1, "name": "Test"}]
""".data(using: .utf8)!
let mock = MockNetworkSession()
mock.mockData = json
mock.mockResponse = HTTPURLResponse(url: URL(string: "https://api.example.com")!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)
let service = NetworkService(session: mock)
let items = try await service.fetchItems()
XCTAssertEqual(items.count, 1)
XCTAssertEqual(items.first?.name, "Test")
}
```
Test @Observable state changes:
```swift
func testAppStateUpdatesOnAdd() {
let appState = AppState()
XCTAssertEqual(appState.items.count, 0)
appState.addItem(Item(name: "Test"))
XCTAssertEqual(appState.items.count, 1)
XCTAssertEqual(appState.items.first?.name, "Test")
}
func testSelectionChanges() {
let appState = AppState()
let item = Item(name: "Test")
appState.addItem(item)
appState.selectedItemID = item.id
XCTAssertEqual(appState.selectedItem?.id, item.id)
}
```
Use XCUITest for critical user flows:
```swift
class MyAppUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
app = XCUIApplication()
app.launch()
}
func testAddItemFlow() {
app.buttons["Add"].click()
let nameField = app.textFields["Name"]
nameField.click()
nameField.typeText("New Item")
app.buttons["Save"].click()
XCTAssertTrue(app.staticTexts["New Item"].exists)
}
}
```
Keep UI tests minimal (slow, brittle). Test critical flows only.
Don't test:
- SwiftUI framework itself
- URLSession (Apple's code)
- File system (use mocks)
Do test:
- Your business logic
- State management
- Data transformations
- Service layer with mocks
```bash
# Run all tests
xcodebuild test -scheme MyApp -destination 'platform=macOS'
# Run unit tests only (fast)
xcodebuild test -scheme MyApp -destination 'platform=macOS' -only-testing:MyAppTests
# Run UI tests only (slow)
xcodebuild test -scheme MyApp -destination 'platform=macOS' -only-testing:MyAppUITests
# Watch mode
find . -name "*.swift" | entr xcodebuild test -scheme MyApp -destination 'platform=macOS' -only-testing:MyAppTests
```