Loading...
Loading...
Use when writing unit tests, adopting Swift Testing framework, making tests run faster without simulator, architecting code for testability, testing async code reliably, or migrating from XCTest - covers @Test/@Suite macros,
npx skill4agent add charleswiltgen/axiom axiom-swift-testing@Test#expect| Configuration | Typical Time | Use Case |
|---|---|---|
| ~0.1s | Pure logic, models, algorithms |
| Host Application: None | ~3s | Framework code, no UI dependencies |
| Bypass app launch | ~6s | App target but skip initialization |
| Full app launch | 20-60s | UI tests, integration tests |
swift testimport Testing
@Test func videoHasCorrectMetadata() {
let video = Video(named: "example.mp4")
#expect(video.duration == 120)
}test@Testasyncthrows// Basic expectation — test continues on failure
#expect(result == expected)
#expect(array.isEmpty)
#expect(numbers.contains(42))
// Required expectation — test stops on failure
let user = try #require(await fetchUser(id: 123))
#expect(user.name == "Alice")
// Unwrap optionals safely
let first = try #require(items.first)
#expect(first.isValid)// Expect any error
#expect(throws: (any Error).self) {
try dangerousOperation()
}
// Expect specific error type
#expect(throws: NetworkError.self) {
try fetchData()
}
// Expect specific error value
#expect(throws: ValidationError.invalidEmail) {
try validate(email: "not-an-email")
}
// Custom validation
#expect {
try process(data)
} throws: { error in
guard let networkError = error as? NetworkError else { return false }
return networkError.statusCode == 404
}@Suite("Video Processing Tests")
struct VideoTests {
let video = Video(named: "sample.mp4") // Fresh instance per test
@Test func hasCorrectDuration() {
#expect(video.duration == 120)
}
@Test func hasCorrectResolution() {
#expect(video.resolution == CGSize(width: 1920, height: 1080))
}
}@Testinitdeinit// Display name
@Test("User can log in with valid credentials")
func loginWithValidCredentials() { }
// Disable with reason
@Test(.disabled("Waiting for backend fix"))
func brokenFeature() { }
// Conditional execution
@Test(.enabled(if: FeatureFlags.newUIEnabled))
func newUITest() { }
// Time limit
@Test(.timeLimit(.minutes(1)))
func longRunningTest() async { }
// Bug reference
@Test(.bug("https://github.com/org/repo/issues/123", "Flaky on CI"))
func sometimesFailingTest() { }
// OS version requirement
@available(iOS 18, *)
@Test func iOS18OnlyFeature() { }// Define tags
extension Tag {
@Tag static var networking: Self
@Tag static var performance: Self
@Tag static var slow: Self
}
// Apply to tests
@Test(.tags(.networking, .slow))
func networkIntegrationTest() async { }
// Apply to entire suite
@Suite(.tags(.performance))
struct PerformanceTests {
@Test func benchmarkSort() { } // Inherits .performance tag
}// ❌ Before: Repetitive
@Test func vanillaHasNoNuts() {
#expect(!IceCream.vanilla.containsNuts)
}
@Test func chocolateHasNoNuts() {
#expect(!IceCream.chocolate.containsNuts)
}
@Test func almondHasNuts() {
#expect(IceCream.almond.containsNuts)
}
// ✅ After: Parameterized
@Test(arguments: [IceCream.vanilla, .chocolate, .strawberry])
func flavorWithoutNuts(_ flavor: IceCream) {
#expect(!flavor.containsNuts)
}
@Test(arguments: [IceCream.almond, .pistachio])
func flavorWithNuts(_ flavor: IceCream) {
#expect(flavor.containsNuts)
}// Test all combinations (4 × 3 = 12 test cases)
@Test(arguments: [1, 2, 3, 4], ["a", "b", "c"])
func allCombinations(number: Int, letter: String) {
// Tests: (1,"a"), (1,"b"), (1,"c"), (2,"a"), ...
}
// Test paired values only (3 test cases)
@Test(arguments: zip([1, 2, 3], ["one", "two", "three"]))
func pairedValues(number: Int, name: String) {
// Tests: (1,"one"), (2,"two"), (3,"three")
}| For-Loop | Parameterized |
|---|---|
| Stops on first failure | All arguments run |
| Unclear which value failed | Each argument shown separately |
| Sequential execution | Parallel execution |
| Can't re-run single case | Re-run individual arguments |
MyApp/
├── MyApp/ # App target (UI, app lifecycle)
├── MyAppCore/ # Swift Package (testable logic)
│ ├── Package.swift
│ └── Sources/
│ └── MyAppCore/
│ ├── Models/
│ ├── Services/
│ └── Utilities/
└── MyAppCoreTests/ # Package testsswift testcd MyAppCore
swift test # Runs in ~0.1 secondsProject Settings → Test Target → Testing
Host Application: None ← Key setting
☐ Allow testing Host Application APIs// Simple solution (no custom startup code)
@main
struct ProductionApp: App {
var body: some Scene {
WindowGroup {
if !isRunningTests {
ContentView()
}
}
}
private var isRunningTests: Bool {
NSClassFromString("XCTestCase") != nil
}
}// Thorough solution (custom startup code)
@main
struct MainEntryPoint {
static func main() {
if NSClassFromString("XCTestCase") != nil {
TestApp.main() // Empty app for tests
} else {
ProductionApp.main()
}
}
}
struct TestApp: App {
var body: some Scene {
WindowGroup { } // Empty
}
}@Test func fetchUserReturnsData() async throws {
let user = try await userService.fetch(id: 123)
#expect(user.name == "Alice")
}// Convert completion handler to async
@Test func legacyAPIWorks() async throws {
let result = try await withCheckedThrowingContinuation { continuation in
legacyService.fetchData { result in
continuation.resume(with: result)
}
}
#expect(result.count > 0)
}@Test func cookiesAreEaten() async {
await confirmation("cookie eaten", expectedCount: 10) { confirm in
let jar = CookieJar(count: 10)
jar.onCookieEaten = { confirm() }
await jar.eatAll()
}
}
// Confirm something never happens
await confirmation(expectedCount: 0) { confirm in
let cache = Cache()
cache.onEviction = { confirm() }
cache.store("small-item") // Should not trigger eviction
}// ❌ Flaky: Task scheduling is unpredictable
@Test func loadingStateChanges() async {
let model = ViewModel()
let task = Task { await model.loadData() }
#expect(model.isLoading == true) // Often fails!
await task.value
}swift-concurrency-extrasimport ConcurrencyExtras
@Test func loadingStateChanges() async {
await withMainSerialExecutor {
let model = ViewModel()
let task = Task { await model.loadData() }
await Task.yield()
#expect(model.isLoading == true) // Deterministic!
await task.value
#expect(model.isLoading == false)
}
}swift-clocksimport Clocks
@MainActor
class FeatureModel: ObservableObject {
@Published var count = 0
let clock: any Clock<Duration>
var timerTask: Task<Void, Error>?
init(clock: any Clock<Duration>) {
self.clock = clock
}
func startTimer() {
timerTask = Task {
while true {
try await clock.sleep(for: .seconds(1))
count += 1
}
}
}
}
// Test with controlled time
@Test func timerIncrements() async {
let clock = TestClock()
let model = FeatureModel(clock: clock)
model.startTimer()
await clock.advance(by: .seconds(1))
#expect(model.count == 1)
await clock.advance(by: .seconds(4))
#expect(model.count == 5)
model.timerTask?.cancel()
}TestClockImmediateClockUnimplementedClock// Serialize tests in a suite that share external state
@Suite(.serialized)
struct DatabaseTests {
@Test func createUser() { }
@Test func deleteUser() { } // Runs after createUser
}
// Serialize parameterized test cases
@Test(.serialized, arguments: [1, 2, 3])
func sequentialProcessing(value: Int) { }// ❌ Bug: Tests depend on execution order
@Suite struct CookieTests {
static var cookie: Cookie?
@Test func bakeCookie() {
Self.cookie = Cookie() // Sets shared state
}
@Test func eatCookie() {
#expect(Self.cookie != nil) // Fails if runs first!
}
}
// ✅ Fixed: Each test is independent
@Suite struct CookieTests {
@Test func bakeCookie() {
let cookie = Cookie()
#expect(cookie.isBaked)
}
@Test func eatCookie() {
let cookie = Cookie()
cookie.eat()
#expect(cookie.isEaten)
}
}@Test func featureUnderDevelopment() {
withKnownIssue("Backend not ready yet") {
try callUnfinishedAPI()
}
}
// Conditional known issue
@Test func platformSpecificBug() {
withKnownIssue("Fails on iOS 17.0") {
try reproduceEdgeCaseBug()
} when: {
ProcessInfo().operatingSystemVersion.majorVersion == 17
}
}| XCTest | Swift Testing |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
@Test// Don't mix XCTest and Swift Testing
@Test func badExample() {
XCTAssertEqual(1, 1) // ❌ Wrong framework
#expect(1 == 1) // ✅ Use this
}// ❌ Avoid: Reference semantics can cause shared state bugs
@Suite class VideoTests { }
// ✅ Prefer: Value semantics isolate each test
@Suite struct VideoTests { }// ❌ May fail with Swift 6 strict concurrency
@Test func updateUI() async {
viewModel.updateTitle("New") // Data race warning
}
// ✅ Isolate to main actor
@Test @MainActor func updateUI() async {
viewModel.updateTitle("New")
}// ❌ Don't serialize just because tests use async
@Suite(.serialized) struct APITests { } // Defeats parallelism
// ✅ Only serialize when tests truly share mutable statedefault-actor-isolation = MainActor// ❌ Error: Main actor-isolated initializer 'init()' has different
// actor isolation from nonisolated overridden declaration
final class PlaygroundTests: XCTestCase {
override func setUp() async throws {
try await super.setUp()
}
}nonisolated// ✅ Works with MainActor default isolation
nonisolated final class PlaygroundTests: XCTestCase {
@MainActor
override func setUp() async throws {
try await super.setUp()
}
@Test @MainActor
func testSomething() async {
// Individual tests can be @MainActor
}
}nonisolated@Suite structTest Plan → Options → Parallelization → "Swift Testing Only"Scheme → Edit Scheme → Test → Info → ☐ DebuggerBuild Settings → Debug Information Format
Debug: DWARF
Release: DWARF with dSYM File@Test#expect#require.tags()asyncawaitconfirmation()withMainSerialExecutor.serialized