Skip to main content

fixing-flaky-tests

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".

Quellinformationen

Repository
bitwarden/ios
Letzte Quellaktivität
16. Juli 2026 um 20:11
Erkannte Sprache von SKILL.md
Englisch
Sterne
694
Forks
160

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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 <testFunctionName>" . --include="*.swift" -l ``` Note that: - **XCTest** functions are named `test_<methodName>_<description>` (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 "<TestClassName>" 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 <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 <SCHEME> \ -only-testing:"<TARGET_NAME>/<TestClassName>/<testFunctionName>" \ -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 ``` `<TARGET_NAME>` is the test bundle (e.g., `BitwardenSharedTests`), `<TestClassName>` is the class (e.g., `MyProcessorTests`), and `<testFunctionName>` 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 <SCHEME> \ -testPlan <SCHEME>-Unit \ -destination "platform=iOS Simulator,name=$DEVICE,OS=$OS" \ 2>&1 | grep -E "(Test Suite|error:|FAILED|Executed)" | tail -30 ``` The `<SCHEME>-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 |
Auf GitHub ansehen