"Three ways to test is not a mess if each one has a job."
The House Rule: Tests by Purpose
The family's apps once used all three styles more or less by accident. The rule now assigns each one a job:
- Swift Testing for SwiftPM package suites.
@Testfunctions,#expectand#require,@Suitetypes, parameterized tests witharguments:, and traits such as.timeLimit(.minutes(1)). It runs withswift test, in parallel by default, and it is where new tests go. - XCTest for Xcode-hosted tests. Unit tests that run inside an iOS or watch app host, extension tests, and UI tests with XCUITest — the XcodeGen targets. Xcode-hosted suites and the upload pipeline's test gate are built around it.
- A self-test executable only for Macs without Xcode. A Mac with only the Command Line Tools has no XCTest, and older tools had no Testing module either. Measured with the 6.1 Command Line Tools, a Swift Testing suite does run there. The family's dictation and text-transformation utilities keep a
-selftestexecutable — plain checks, exit status 1 on failure — so their pure logic can be verified there, and the repository says so.
What Swift 6.3 Added
Swift Testing in 6.3 can record a warning issue (Issue.record("…", severity: .warning)) that is reported without failing the test, cancel a test that cannot meaningfully run (try Test.cancel()), and attach images to results. Exit tests and attachments arrived in 6.2.
Traps the Family Paid For
- A hosted test boots the whole app. An iOS unit test target runs inside the app, so
Bundle.mainis right — butUserDefaults.standardis the app's, and the app launched beside the tests dials its real engine endpoint. Inject the defaults store, the notification centre and transport construction. - AppKit tests share focus. Tests that create windows and key views interfere when run in parallel; a family editor runs its suite with
swift test --no-parallel. - A symptom that moves under an inert edit is not a code bug. Appending an unrelated two-assertion
@Suiteto a file made a real-clock test in the same file abort the whole test process with a task-allocator failure, eight runs out of eight. Moving the new suite to its own file fixed it. The expensive part was bisecting: twice the investigation rewrote working production code because each intermediate result looked causal. Rule: when a "fix" changes the symptom but an inert edit changes it too, stop bisecting and rule things out one at a time, with a fixed number of repetitions per configuration. - An async test that can hang needs a time limit. Without a
.timeLimittrait, a regression that never resumes stalls the entire run instead of failing one test.