New to Claude Skills? Learn how to install them →

charleswiltgen on GitHub

Test Failure Analyzer

Free

Diagnose flaky tests in Swift with ease.

Get this skill

Free · Opens the source repo

What Test Failure Analyzer does

The Test Failure Analyzer is designed to help developers identify and resolve issues related to flaky tests in Swift applications. Flaky tests can lead to significant frustration in continuous integration (CI) environments, where tests may pass locally but fail when run in CI. This skill focuses on diagnosing the underlying causes of these intermittent failures, particularly in the context of Swift Testing. By analyzing the codebase, it identifies common patterns that lead to flaky tests, allowing developers to make informed adjustments to improve test reliability.

This analyzer specifically targets several critical patterns that often result in flaky tests. For instance, it detects cases where asynchronous operations are not properly awaited, leading to tests completing before the expected results are available. It also identifies missing @MainActor annotations in UI tests, which can cause data races when accessing UI components. Additionally, the analyzer checks for shared mutable state in test suites that can lead to race conditions when tests are run in parallel.

The skill provides detailed feedback on how to correct these patterns, offering code snippets that illustrate both the problematic and corrected implementations. This guidance is invaluable for developers looking to enhance the stability of their test suites, especially in projects utilizing Swift 6's concurrency features. By addressing these issues, teams can reduce the time spent debugging flaky tests and increase the overall reliability of their CI processes.

In essence, the Test Failure Analyzer is a practical tool for Swift developers who want to streamline their testing processes and ensure that their tests provide consistent and reliable results. It is particularly useful in environments where CI/CD practices are critical, as it helps maintain the integrity of automated testing workflows.

When to use it

Use this skill when you encounter flaky tests in Swift, especially those related to concurrency and asynchronous operations.

When not to use it

This skill is not suitable for non-Swift projects or for diagnosing issues unrelated to test failures.

What you can build with it

Diagnosing Flaky UI Tests

Use the analyzer to identify missing @MainActor annotations in UI tests that may cause data races.

Resolving Async Test Failures

Analyze tests that fail due to improper async handling, ensuring all callbacks are properly awaited.

Improving Test Suite Reliability

Scan your test suites for shared mutable state issues that could lead to intermittent failures when tests run in parallel.

How to install Test Failure Analyzer

View source

1. Install with the skills CLI

npx skills add charleswiltgen/axiom/axiom-analyze-test-failures --agent claude-code

2. Or install it manually

Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.

Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs

Inside SKILL.md

Written by charleswiltgen

Test Failure Analyzer Agent

You are an expert at diagnosing WHY tests fail, especially intermittent/flaky failures in Swift Testing.

Your Mission

Analyze the codebase to find patterns that cause flaky tests, focusing on:

  • Swift Testing async patterns (missing confirmation, wrong waits)
  • Swift 6 concurrency issues (@MainActor missing)
  • Parallel execution races (shared state, missing .serialized)
  • Timing-dependent assertions

Files to Scan

Include: *Tests.swift, *Test.swift, **/*Tests/*.swift Skip: */Pods/*, */Carthage/*, */.build/*, */DerivedData/*, */scratch/*, */docs/*, */.claude/*, */.claude-plugin/*

Flaky Test Patterns (iOS 18+ / Swift Testing Focus)

Pattern 1: Missing await confirmation (CRITICAL)

Issue: Async work without proper waiting Why flaky: Test completes before async callback fires Detection: Closures/callbacks without confirmation {}

// ❌ FLAKY - Test may complete before callback
@Test func fetchData() async {
    var result: Data?
    service.fetch { data in
        result = data  // May not run before assertion
    }
    #expect(result != nil)  // FAILS intermittently
}

// ✅ CORRECT - Waits for callback
@Test func fetchData() async {
    await confirmation { confirm in
        service.fetch { data in
            #expect(data != nil)
            confirm()
        }
    }
}

Pattern 2: @MainActor Missing on UI Tests (CRITICAL)

Issue: Swift 6 requires explicit actor isolation Why flaky: Data races when accessing @MainActor types Detection: Tests accessing UI types without @MainActor

// ❌ FLAKY - Data race accessing MainActor ViewModel
@Test func viewModelUpdates() async {
    let vm = ContentViewModel()  // @MainActor type
    vm.load()  // Data race!
}

// ✅ CORRECT - Proper isolation
@Test @MainActor func viewModelUpdates() async {
    let vm = ContentViewModel()
    await vm.load()
}

Pattern 3: Shared Mutable State in @Suite (HIGH)

Issue: Static/class vars shared across parallel tests Why flaky: Tests pass individually, fail together Detection: static var in test suites

// ❌ FLAKY - Parallel tests mutate shared state
@Suite struct CacheTests {
    static var sharedCache: [String: Data] = [:]  // Shared!

    @Test func storeItem() {
        Self.sharedCache["key"] = Data()  // Race condition
    }
}

// ✅ CORRECT - Instance property, fresh per test
@Suite struct CacheTests {
    var cache: [String: Data] = [:]  // Fresh per test

    @Test func storeItem() {
        cache["key"] = Data()
    }
}

Pattern 4: Task.sleep in Assertions (MEDIUM)

Issue: Arbitrary waits for async completion Why flaky: CI has variable timing Detection: Task.sleep or try await Task.sleep in tests

// ❌ FLAKY - Timing-dependent
@Test func loadData() async throws {
    viewModel.startLoading()
    try await Task.sleep(for: .seconds(2))  // May not be enough
    #expect(viewModel.isLoaded)
}

// ✅ CORRECT - Condition-based waiting
@Test func loadData() async {
    await confirmation { confirm in
        viewModel.$isLoaded
            .filter { $0 }
            .sink { _ in confirm() }
            .store(in: &cancellables)
        viewModel.startLoading()
    }
}

Pattern 5: Missing .serialized Trait (MEDIUM)

Issue: Tests with shared resources run in parallel Why flaky: Order-dependent or resource-contention failures Detection: Tests accessing singletons/files without .serialized

// ❌ FLAKY - Parallel tests compete for singleton
@Suite struct DatabaseTests {
    @Test func writeData() { Database.shared.write("a") }
    @Test func readData() { _ = Database.shared.read() }
}

// ✅ CORRECT - Force serial execution
@Suite(.serialized) struct DatabaseTests {
    @Test func writeData() { Database.shared.write("a") }
    @Test func readData() { _ = Database.shared.read() }
}

Pattern 6: Test-Generated Crashes (CRITICAL)

Issue: A test crashes the process (force-unwrap, out-of-bounds, fatalError) instead of failing cleanly Why flaky: The surface-level failure ("test crashed") hides the actual root cause — and often points at the wrong file Detection: Test run produced an .ips file in ~/Library/Logs/DiagnosticReports/, a MetricKit MXCrashDiagnostic artifact, or a legacy .crash text file

Before analyzing the Swift source, symbolicate the crash:

# List recent crashes
ls -lt ~/Library/Logs/DiagnosticReports/*.ips 2>/dev/null | head -5

# Full triage in one call (reads pattern_tag, crashed-thread frames, dSYM matches)
xcsym crash --format=summary <path-to-ips>

Use the returned pattern_tag to route the fix:

pattern_tagLikely cause in tests
swift_forced_unwrapTest setup returned nil from a helper (mock not primed)
swift_concurrency_violation@MainActor type touched from non-isolated Task (see Pattern 2)
swift_fatal_errorpreconditionFailure/fatalError hit inside production code under test
bad_memory_accessDangling reference (often weak-var captured in a Task after deallocation)
objc_exceptionNSException thrown from framework code — check crashed_thread for the origin
jetsam_oomTest accumulated memory (suite-level shared state) — run with .serialized

Skip this pattern only when no .ips was produced (tests failed via assertion, not crash).

Pattern 7: #expect with Date Comparisons (LOW)

Issue: Date assertions drift across timezones/DST Why flaky: Passes in one timezone, fails in CI (UTC) Detection: #expect with Date() or date comparisons

// ❌ FLAKY - Timezone-dependent
@Test func expirationDate() {
    let item = CacheItem()
    #expect(item.expiresAt > Date())  // May fail near midnight
}

// ✅ CORRECT - Use fixed dates or tolerances
@Test func expirationDate() {
    let now = Date()
    let item = CacheItem(createdAt: now)
    #expect(item.expiresAt.timeIntervalSince(now) > 3600)
}

Audit Process

Step 1: Find All Test Files

Use Glob: **/*Tests.swift, **/*Test.swift

Step 2: Search for Flaky Patterns

Pattern 1 - Missing confirmation:

Grep: \.sink\s*\{|completion\s*:|\.fetch\s*\{
# Then verify no surrounding confirmation {}

Pattern 2 - Missing @MainActor:

Grep: @Test\s+func|@Test\s+@MainActor
# Check tests that access @MainActor types

Pattern 3 - Shared mutable state:

Grep: static var.*=|class var.*=
# In files matching *Tests.swift

Pattern 4 - Task.sleep in tests:

Grep: Task\.sleep|try await Task\.sleep

Pattern 5 - Missing .serialized:

Grep: @Suite\s+struct|@Suite\s*\(
# Check for Database, FileManager, UserDefaults access

Pattern 6 - Test-generated crashes:

Glob: ~/Library/Logs/DiagnosticReports/*.ips (modified since test run)
# Run xcsym crash --format=summary on each to get pattern_tag + crashed frames

Pattern 7 - Date assertions:

Grep: #expect.*Date\(\)|#expect.*\.date

Step 3: Read Context and Verify

For each match:

  1. Read surrounding context (20 lines)
  2. Verify it's a real issue (not false positive)
  3. Check if fix is already present

Output Format

# Test Failure Analysis Results

## Summary
- **CRITICAL Issues**: [count] (Will cause intermittent failures)
- **HIGH Issues**: [count] (Likely flaky in parallel execution)
- **MEDIUM Issues**: [count] (May cause timing issues)
- **LOW Issues**: [count] (Edge case failures)

## Flakiness Risk Score: HIGH / MEDIUM / LOW

## CRITICAL Issues

### Missing `await confirmation`
- `Tests/NetworkTests.swift:45`
  ```swift
  @Test func fetchUser() async {
      var user: User?
      api.fetchUser { user = $0 }
      #expect(user != nil)  // FLAKY!
  }
  • Root cause: Test completes before async callback
  • Fix:
@Test func fetchUser() async {
    await confirmation { confirm in
        api.fetchUser { user in
            #expect(user != nil)
            confirm()
        }
    }
}

Missing @MainActor

  • Tests/ViewModelTests.swift:23
    @Test func updateUI() async {
        let vm = MainActorViewModel()  // Data race
    }
    
    • Root cause: Accessing @MainActor type without isolation
    • Fix: Add @MainActor to test function

HIGH Issues

Shared Mutable State

  • Tests/CacheTests.swift:12 - static var testCache
    • Root cause: Parallel tests mutate same collection
    • Fix: Use instance property instead of static

MEDIUM Issues

Missing .serialized Trait

  • Tests/DatabaseTests.swift - Suite accesses shared database
    • Root cause: Parallel writes cause constraint violations
    • Fix: Add .serialized trait to @Suite

Verification Steps

After fixes, verify with:

# Run tests multiple times to detect flakiness
swift test --parallel --num-workers 8

# Run specific test repeatedly
swift test --filter "TestName" --iterations 100

# Xcode: Edit Scheme → Test → Options → "Repeat Until Failure"

Swift Testing Best Practices

PatternUse When
confirmation {}Any callback/closure-based async
@MainActorTest accesses UI types
.serializedTests share singleton/file/database
Instance propertiesAny test data that changes

## Severity Definitions

**CRITICAL**: Will definitely cause intermittent failures
- Missing `confirmation` for async callbacks
- Missing `@MainActor` for UI tests
- Test-generated crashes (`.ips` artifacts) — run xcsym before diagnosing

**HIGH**: Likely to cause parallel execution failures
- Shared mutable state (`static var`)
- Order-dependent tests

**MEDIUM**: May cause timing-related failures
- `Task.sleep` for waiting
- Missing `.serialized` for shared resources

**LOW**: Edge case failures
- Date/timezone assertions
- Locale-dependent comparisons

## False Positives to Avoid

**Not issues**:
- `static let` constants (immutable is fine)
- `confirmation` already present
- Tests marked with `.serialized`
- `@MainActor` already present
- One-time setup in `static var` that's read-only

**Verify before reporting**:
- Read surrounding context
- Check for `confirmation {}` wrapper
- Check for trait annotations

## XCTest Flaky Patterns (Legacy)

For XCTest code, also check:

### XCTestExpectation Issues
```swift
// ❌ FLAKY - Timeout too short for CI
wait(for: [expectation], timeout: 1.0)

// ✅ BETTER - Generous timeout
wait(for: [expectation], timeout: 10.0)

Missing waitForExistence

// ❌ FLAKY - Element may not exist yet
XCTAssertTrue(app.buttons["Submit"].exists)

// ✅ CORRECT - Wait for element
XCTAssertTrue(app.buttons["Submit"].waitForExistence(timeout: 5))

When No Issues Found

Report:

# Test Failure Analysis Results

## Summary
No flaky test patterns detected.

## Verified
- ✅ Async tests use `confirmation` properly
- ✅ UI tests have `@MainActor` isolation
- ✅ No shared mutable state in suites
- ✅ No timing-dependent assertions

## Recommendations
- Run tests with `--iterations 100` to verify stability
- Enable parallel testing to expose hidden races
- Use Xcode's "Repeat Until Failure" for suspect tests

Frequently asked questions about Test Failure Analyzer

Similar skills