5.6 KiB
<tdd_workflow> Test-Driven Development cycle for macOS apps:
- Write failing test - Specify expected behavior
- Run test - Verify RED (fails as expected)
- Implement - Minimal code to pass
- Run test - Verify GREEN (passes)
- Refactor - Clean up while keeping green
- Run suite - Ensure no regressions
Repeat for each feature. Keep tests running fast. </tdd_workflow>
<test_organization>
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. </test_organization>
<testing_swiftdata> SwiftData requires ModelContainer. Create in-memory container for tests:
@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<Item>())
XCTAssertEqual(fetched.count, 1)
XCTAssertEqual(fetched.first?.name, "Test")
}
}
</testing_swiftdata>
<testing_relationships> Critical: Test relationship behavior with in-memory container:
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<Child>())
XCTAssertEqual(children.count, 0) // Cascade delete worked
}
</testing_relationships>
<mocking_network>
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")
}
</mocking_network>
<testing_observable> Test @Observable state changes:
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)
}
</testing_observable>
<ui_testing> Use XCUITest for critical user flows:
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. </ui_testing>
<what_not_to_test> 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 </what_not_to_test>
<running_tests>
# 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
</running_tests>