--- name: fixing-flaky-tests description: > Diagnose and fix flaky (intermittently failing) tests in Bitwarden iOS. Finds the root cause of non-deterministic test failures (race conditions, timing issues, shared state, order dependence), applies a targeted fix, then stress-tests the fix by running the test 100 times to confirm stability. Use this skill whenever a test is failing intermittently, sometimes passes / sometimes fails, or someone describes a test as flaky, unstable, or non-deterministic — even if the exact cause is unknown. Trigger phrases: "fix flaky test", "test is flaky", "test keeps failing", "intermittent failure", "test is non-deterministic", "test fails sometimes", "test randomly fails". --- # Fixing Flaky Tests — Bitwarden iOS A flaky test passes sometimes and fails other times without any code change. Flakiness usually comes from a small set of root causes. Your job is to find which one applies, fix it surgically, and then prove the fix holds by running the test many times. ## Step 1: Locate the test(s) Search for the function name(s) the user provided: ```bash grep -r "func " . --include="*.swift" -l ``` Note that: - **XCTest** functions are named `test__` (prefix required by XCTest) - **Swift Testing** functions use the `@Test` macro and can have any name — the `test_` prefix is conventional but not required For each test: - Read the full test function and the class it belongs to - Read the `@testable import` at the top of the file — that's the module under test (e.g., `@testable import BitwardenShared` → module `BitwardenShared`) - Read the SUT code being exercised by the test ## Step 2: Map the test to a scheme and test plan | Test target | Scheme | Unit test plan | |---|---|---| | `BitwardenSharedTests`, `BitwardenTests`, `BitwardenAutoFillExtensionTests`, `BitwardenActionExtensionTests`, `BitwardenShareExtensionTests` | `Bitwarden` | `Bitwarden-Unit` | | `AuthenticatorSharedTests`, `AuthenticatorTests`, `AuthenticatorBridgeKitTests` | `Authenticator` | `Authenticator-Unit` | | `BitwardenKitTests` | `BitwardenKit` | `BitwardenKit-Unit` | | `BitwardenKitViewInspectorTests` | `BitwardenKit` | `BitwardenKit-ViewInspector` | | `NetworkingTests` | `BitwardenKit` | `BitwardenKit-Default` | When in doubt, search `TestPlans/` for the test class name to confirm the plan (and therefore scheme): ```bash grep -r "" TestPlans/ -l ``` Always read the simulator config from the project files rather than hardcoding: ```bash DEVICE=$(tr -d '\n' < .test-simulator-device-name) OS=$(tr -d '\n' < .test-simulator-ios-version) ``` ## Step 3: Diagnose the root cause Read the test carefully and the SUT it exercises. Flaky tests in Swift/iOS typically fall into one of these categories: **Race condition / async ordering** The test observes a side effect before the async SUT has finished producing it. Signs: bare `Task.sleep` or `DispatchQueue.asyncAfter` in the test, expectations that time out occasionally, `@MainActor`-isolated code whose scheduling the test doesn't account for. **Shared mutable state** State from one test bleeds into the next. Signs: static properties or singletons not reset in `tearDown`, `NotificationCenter` observers not removed, `Task` or `DispatchQueue.async` work from a previous test still running when the next one starts. **Time / date dependence** The SUT reads `Date()` or `Calendar.current` inline, so the test result depends on the real clock. Signs: test passes during the day but fails near midnight, or fails only under heavy load when wall-clock timing drifts. **Order dependence** Test execution order is randomized in the -Default test plans ("testExecutionOrdering" : "random"), but the -Unit plans used for verification here run sequentially. To reproduce order-dependent flakiness, run against the -Default plan rather than -Unit. **Resource contention** CoreData, Keychain, or file system state left behind by a previous run. ## Step 4: Fix the test (or plan the SUT change) ### Fixes in the test file only — apply and move on **Replacing bare delays with condition polling:** Use the project's test helpers instead of a fixed sleep. These are available to all test targets: - **XCTest** (subclasses of `BaseBitwardenTestCase`): - `waitFor { condition }` — spins the run loop; use when the SUT uses `Timer.scheduledTimer` - `waitForAsync { condition }` — polls asynchronously; use for Swift Concurrency-based SUT code - **Swift Testing** (`SwiftTestingHelpers.swift`): - `try await waitForAsync { condition }` — polls asynchronously in `@Test` functions - `waitFor(condition)` — spins the run loop in `@Test` functions using `@MainActor` - `await withContinuationTimeout { resume in … }` — wraps callback-based async code so the test fails cleanly rather than hanging if the callback is never called **Shared state:** - Move subject and dependency properties from `static var` to instance `var` - Reset all mutable properties in `tearDown` / `addTeardownBlock` - Remove `NotificationCenter` observers: `addTeardownBlock { NotificationCenter.default.removeObserver(token) }` **CoreData / Keychain leaks:** - Use `.memory` store type when constructing `AuthenticatorBridgeDataStore` or any CoreData stack in tests so nothing persists to disk between runs ### If the fix requires changing the SUT (production code) Stop before touching production code. Present the plan: 1. The root cause 2. The specific SUT file, line, and change (what pattern to replace with what) 3. Why a test-only fix is insufficient Then ask: *"Would you like me to proceed, or would you prefer to handle the SUT change separately?"* The most common SUT fix is replacing an inline `Date()` or `Calendar.current` call with an injected `TimeProvider` (protocol in `BitwardenKit/Core/Platform/Services/TimeProvider.swift`), then using `MockTimeProvider` in the test. Only proceed with SUT edits after the user agrees. ## Step 5: Verify the fix — run the test 100 times Build first so the changes are compiled: ```bash xcodebuild build-for-testing \ -workspace Bitwarden.xcworkspace \ -scheme \ -destination "platform=iOS Simulator,name=$DEVICE,OS=$OS" \ 2>&1 | grep -E "error:|BUILD (SUCCEEDED|FAILED)" ``` Then stress-test the fixed test using `-test-iterations`, which runs N repetitions in a single simulator session (far faster than re-launching xcodebuild 100 times): ```bash xcodebuild test-without-building \ -workspace Bitwarden.xcworkspace \ -scheme \ -only-testing:"//" \ -test-iterations 100 \ -test-repetition-mode count-up \ -destination "platform=iOS Simulator,name=$DEVICE,OS=$OS" \ 2>&1 | grep -E "(Test Case|Executed|error:|FAILED)" | tail -20 ``` `` is the test bundle (e.g., `BitwardenSharedTests`), `` is the class (e.g., `MyProcessorTests`), and `` is the Swift function name exactly as written. **Interpreting results:** - `Executed 100 tests, with 0 failures` → fix holds, proceed to Step 6 - Any failure → return to Step 3; the root cause wasn't fully addressed ## Step 6: Run the full unit test suite (once) Confirm the fix didn't accidentally break anything else in the same target: ```bash xcodebuild test \ -workspace Bitwarden.xcworkspace \ -scheme \ -testPlan -Unit \ -destination "platform=iOS Simulator,name=$DEVICE,OS=$OS" \ 2>&1 | grep -E "(Test Suite|error:|FAILED|Executed)" | tail -30 ``` The `-Unit` plan (e.g., `Bitwarden-Unit`) excludes snapshot and ViewInspector tests, keeping the run fast and focused on correctness. If any other test fails, investigate whether the fix introduced a regression before proceeding. ## Step 7: Final verification — run the build-test-verify skill As the last step, invoke the `build-test-verify` skill to confirm the full pipeline is clean (lint, formatter, spell check): ``` Skill: build-test-verify ``` --- ## Quick reference | Symptom | Likely cause | Fix | |---|---|---| | Bare `try await Task.sleep(…)` after triggering async SUT | Side effect races the sleep | Replace with `waitForAsync { … }` | | `Timer.scheduledTimer` result observed immediately | Run loop not spinning | Use `waitFor { … }` (spins run loop) | | Callback never called → hang | Missing continuation timeout | Use `withContinuationTimeout { resume in … }` | | `static var` on subject or dependency | Shared state across tests | Move to instance `var`, reset in `tearDown` | | `NotificationCenter.addObserver` without `removeObserver` | Observer leaks | `addTeardownBlock { NotificationCenter.default.removeObserver(token) }` | | `Date()` / `Calendar.current` inline in SUT | Wall-clock dependence | Inject `TimeProvider`; use `MockTimeProvider` in tests | | `waitForExpectations(timeout: 0.1)` | Timeout too tight under load | Increase timeout or replace with `waitForAsync` | | CoreData state persists across tests | Disk-backed store in tests | Use `.memory` store type |