32 KiB
Testing Guide
Table of Contents
- Testing Philosophy
- Test File Naming and Location
- Decision Tree: What Tests to Write
- Testing Strategies
- Testing by Component Type
- Common Testing Patterns
- Mock Generation and Usage
- Running Tests
- Test Maintenance
- Quick Reference for AI Agents
Testing Philosophy
Every type containing logic must be tested. The test suite should:
- Validate business logic and state management
- Verify user interactions trigger correct behaviors
- Catch visual regressions across display modes
- Enable confident refactoring through comprehensive coverage
Test File Naming and Location
Naming Conventions
- Unit tests:
<TypeToTest>Tests.swift - Snapshot tests:
<TypeToTest>+SnapshotTests.swift - View Inspector tests:
<TypeToTest>+ViewInspectorTests.swift
File Location
Test files must be co-located with the implementation file in the same folder:
BitwardenShared/UI/Platform/Application/
├── AppProcessor.swift
├── AppProcessorTests.swift # Unit tests
├── AppView.swift
├── AppView+SnapshotTests.swift # Snapshot tests
└── AppView+ViewInspectorTests.swift # View inspector tests
This makes it easy to:
- Find tests for a specific type
- Open implementation and tests side-by-side
- Ensure tests evolve with the code
Decision Tree: What Tests to Write
Use this decision tree to determine which tests to write for a new or modified component:
┌─────────────────────────────────────┐
│ What type of component is this? │
└─────────────────────────────────────┘
│
┌────────────┼────────────┐
│ │ │
┌───▼────┐ ┌────▼───┐ ┌─────▼──┐
│ Model │ │ Logic │ │ View │
└───┬────┘ └───┬────┘ └────┬───┘
┌──────────────────────────────────────┐
│ Model (Domain/Request/Response) │
│ ✓ Unit tests for: │
│ - Codable conformance │
│ - Custom init logic │
│ - Computed properties │
│ - Validation methods │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ Logic Component │
│ (Processor/Service/Repository/Store) │
│ ✓ Unit tests for: │
│ - All public methods │
│ - State mutations │
│ - Error handling │
│ - Async operations │
│ - Edge cases │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ View (SwiftUI) │
│ ✓ ViewInspector tests for: │
│ - Button taps send actions │
│ - Toggle changes send actions │
│ - TextField bindings work │
│ - Navigation triggers │
│ ✓ Snapshot tests for: │
│ - Light mode │
│ - Dark mode │
│ - Large dynamic type │
│ - Loading/error/empty states │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ Coordinator │
│ ✓ Unit tests for: │
│ - Route navigation │
│ - Child coordinator creation │
│ - Event handling │
│ - Context passing │
└──────────────────────────────────────┘
Testing Strategies
1. Unit Tests (XCTest / Swift Testing)
Purpose: Validate business logic, state management, and data transformations
Tools:
- XCTest (legacy, still widely used)
- Swift Testing Framework (preferred for new tests)
Use for:
- Processors (state mutations, action handling, effects)
- Services (business logic, data transformations)
- Repositories (data synthesis, error handling)
- Data Stores (persistence operations)
- Models (codable, computed properties, validation)
- Coordinators (navigation logic)
2. ViewInspector Tests
Purpose: Verify user interactions send correct actions/effects through the Store
Tool: ViewInspector
Use for:
- Button taps trigger actions
- Toggle changes send state updates
- TextField bindings work correctly
- Sheet/alert presentations
- Navigation link triggers
3. Snapshot Tests
Purpose: Catch visual regressions across different display modes
Tool: SnapshotTesting
Use for:
- All user-facing views
- Different states (loading, error, empty, populated)
- Accessibility configurations
Required Snapshots:
- ✅ Light mode
- ✅ Dark mode
- ✅ Large dynamic type (accessibility)
Important: Snapshot tests must run on the specific simulator defined in:
- Device: .test-simulator-device-name
- iOS Version: .test-simulator-ios-version
Otherwise, tests will fail due to rendering differences between iOS versions.
Testing by Component Type
Testing Processors
Processors manage state and handle actions/effects. Focus on:
What to Test
- ✅ Initial state is correct
- ✅ Actions update state correctly
- ✅ Effects perform async work and update state
- ✅ Navigation requests are sent to coordinator
- ✅ Error handling updates state appropriately
Example Test Structure
@testable import BitwardenShared
import XCTest
class ExampleProcessorTests: BitwardenTestCase {
var subject: ExampleProcessor!
var coordinator: MockCoordinator<ExampleRoute, Void>!
var exampleRepository: MockExampleRepository!
override func setUp() {
super.setUp()
coordinator = MockCoordinator()
exampleRepository = MockExampleRepository()
subject = ExampleProcessor(
coordinator: coordinator.asAnyCoordinator(),
services: ServiceContainer.withMocks(
exampleRepository: exampleRepository
),
state: ExampleState()
)
}
override func tearDown() {
super.tearDown()
subject = nil
coordinator = nil
exampleRepository = nil
}
// Test action handling
func test_receive_toggleAction_updatesState() {
subject.state.isToggleOn = false
subject.receive(.toggleChanged(true))
XCTAssertTrue(subject.state.isToggleOn)
}
// Test effect handling
func test_perform_loadData_success_updatesState() async {
exampleRepository.loadDataResult = .success("Test Data")
await subject.perform(.loadData)
XCTAssertEqual(subject.state.data, "Test Data")
XCTAssertFalse(subject.state.isLoading)
}
// Test navigation
func test_receive_nextAction_navigatesToNextScreen() {
subject.receive(.next)
XCTAssertEqual(coordinator.routes.last, .nextExample)
}
// Test error handling
func test_perform_loadData_failure_showsError() async {
exampleRepository.loadDataResult = .failure(BitwardenTestError.example)
await subject.perform(.loadData)
XCTAssertNotNil(subject.state.errorAlert)
}
}
Testing Services
Services have discrete responsibilities. Focus on:
What to Test
- ✅ All public method signatures
- ✅ Data transformations
- ✅ Error propagation
- ✅ Interaction with dependencies (use mocks)
- ✅ Edge cases and boundary conditions
Example Pattern
class ExampleServiceTests: BitwardenTestCase {
var subject: DefaultExampleService!
var dataStore: MockDataStore!
var apiService: MockAPIService!
override func setUp() {
super.setUp()
dataStore = MockDataStore()
apiService = MockAPIService()
subject = DefaultExampleService(
dataStore: dataStore,
apiService: apiService
)
}
func test_fetchData_returnsMergedData() async throws {
// Arrange
dataStore.fetchResult = [/* local data */]
apiService.fetchResult = .success(/* remote data */)
// Act
let result = try await subject.fetchData()
// Assert
XCTAssertEqual(result.count, expectedCount)
XCTAssertTrue(dataStore.fetchCalled)
XCTAssertTrue(apiService.fetchCalled)
}
}
Testing Repositories
Repositories synthesize data from multiple sources. Focus on:
What to Test
- ✅ Data synthesis from multiple services
- ✅ Async operation coordination
- ✅ Error handling from various sources
- ✅ Publisher streams emit correct values
- ✅ State synchronization
Example Pattern
class ExampleRepositoryTests: BitwardenTestCase {
var subject: DefaultExampleRepository!
var exampleService: MockExampleService!
var otherService: MockOtherService!
func test_loadData_combinesMultipleSources() async throws {
exampleService.dataResult = .success(data1)
otherService.dataResult = .success(data2)
let result = try await subject.loadData()
// Verify data was combined correctly
XCTAssertEqual(result.combinedField, expectedValue)
}
}
Testing Views
Views render state and send actions. Focus on:
ViewInspector Tests: User Interactions
@testable import BitwardenShared
import ViewInspector
import XCTest
class ExampleViewTests: BitwardenTestCase {
var processor: MockProcessor<ExampleState, ExampleAction, ExampleEffect>!
var subject: ExampleView!
override func setUp() {
super.setUp()
processor = MockProcessor(state: ExampleState())
subject = ExampleView(store: Store(processor: processor))
}
// Test button tap
func test_nextButton_tapped_sendsAction() throws {
let button = try subject.inspect().find(button: "Next")
try button.tap()
XCTAssertEqual(processor.actions.last, .next)
}
// Test toggle
func test_toggle_changed_sendsAction() throws {
let toggle = try subject.inspect().find(ViewType.Toggle.self)
try toggle.tap()
XCTAssertEqual(processor.actions.last, .toggleChanged(true))
}
}
Snapshot Tests: Visual Verification
@testable import BitwardenShared
import SnapshotTesting
import XCTest
class ExampleView_SnapshotTests: BitwardenTestCase {
var subject: ExampleView!
override func setUp() {
super.setUp()
subject = ExampleView(
store: Store(
processor: StateProcessor(
state: ExampleState(/* configure state */)
)
)
)
}
// Test all required modes
func test_snapshot_lightMode() {
assertSnapshot(of: subject, as: .defaultPortrait)
}
func test_snapshot_darkMode() {
assertSnapshot(of: subject, as: .defaultPortraitDark)
}
func test_snapshot_largeDynamicType() {
assertSnapshot(of: subject, as: .defaultPortraitAX5)
}
// Test different states
func test_snapshot_loadingState() {
subject.store.state.isLoading = true
assertSnapshot(of: subject, as: .defaultPortrait)
}
func test_snapshot_errorState() {
subject.store.state.errorMessage = "Something went wrong"
assertSnapshot(of: subject, as: .defaultPortrait)
}
}
Testing Coordinators
Coordinators handle navigation. Focus on:
What to Test
- ✅ Route navigation creates correct views/coordinators
- ✅ Child coordinators are created with correct dependencies
- ✅ Event handling triggers correct routes
- ✅ Context is passed correctly
Example Pattern
class ExampleCoordinatorTests: BitwardenTestCase {
var subject: ExampleCoordinator!
var module: MockAppModule!
var stackNavigator: MockStackNavigator!
override func setUp() {
super.setUp()
module = MockAppModule()
stackNavigator = MockStackNavigator()
subject = ExampleCoordinator(
module: module,
services: ServiceContainer.withMocks(),
stackNavigator: stackNavigator
)
}
func test_navigate_example_showsView() {
subject.navigate(to: .example)
XCTAssertTrue(stackNavigator.pushed)
XCTAssertTrue(stackNavigator.pushedView is ExampleView)
}
func test_navigate_nextExample_createsChildCoordinator() {
subject.navigate(to: .nextExample)
XCTAssertTrue(module.makeNextExampleCoordinatorCalled)
}
}
Common Testing Patterns
Pattern 1: Testing Async Operations
func test_asyncOperation_updatesState() async {
// Setup mock result
mockService.result = .success(expectedData)
// Perform async effect
await subject.perform(.loadData)
// Assert state was updated
XCTAssertEqual(subject.state.data, expectedData)
XCTAssertFalse(subject.state.isLoading)
}
Pattern 2: Testing Error Handling
func test_operation_failure_showsAlert() async {
mockService.result = .failure(TestError.example)
await subject.perform(.loadData)
XCTAssertNotNil(subject.state.alert)
XCTAssertEqual(subject.state.alert?.title, "Error")
}
Pattern 3: Testing Publisher Streams
func test_publisher_emitsCorrectValues() async throws {
var receivedValues: [String] = []
let cancellable = subject.dataPublisher
.sink { value in
receivedValues.append(value)
}
// Trigger updates
await subject.updateData("Value1")
await subject.updateData("Value2")
XCTAssertEqual(receivedValues, ["Value1", "Value2"])
cancellable.cancel()
}
Pattern 4: Testing State Equality
func test_state_equality() {
let state1 = ExampleState(data: "test", isLoading: false)
let state2 = ExampleState(data: "test", isLoading: false)
let state3 = ExampleState(data: "other", isLoading: false)
XCTAssertEqual(state1, state2)
XCTAssertNotEqual(state1, state3)
}
Mock Generation and Usage
Generating Mocks with Sourcery
The codebase uses Sourcery to auto-generate mocks.
Mark a Protocol for Mocking
// sourcery: AutoMockable
protocol ExampleService {
func fetchData() async throws -> [String]
var dataPublisher: AnyPublisher<[String], Never> { get }
}
Generated Mock Location
Mocks are generated in:
BitwardenShared/Core/Platform/Models/Sourcery/AutoMockable.generated.swift
Using Generated Mocks
class ExampleTests: BitwardenTestCase {
var mockService: MockExampleService!
override func setUp() {
super.setUp()
mockService = MockExampleService()
}
func test_example() async throws {
// Setup mock behavior
mockService.fetchDataResult = .success(["data1", "data2"])
// Use in system under test
let result = try await mockService.fetchData()
// Verify
XCTAssertEqual(result.count, 2)
XCTAssertTrue(mockService.fetchDataCalled)
}
}
ServiceContainer with Mocks
Use ServiceContainer.withMocks() to inject dependencies:
let services = ServiceContainer.withMocks(
exampleRepository: mockExampleRepository,
exampleService: mockExampleService
)
let processor = ExampleProcessor(
coordinator: coordinator.asAnyCoordinator(),
services: services,
state: ExampleState()
)
Running Tests
Test Plans
Test plans are organized in the TestPlans folder. Each project has multiple test plans to allow running specific subsets of tests:
Test Plan Structure
{ProjectName}-Default.xctestplan: All tests (unit, snapshot, view inspector){ProjectName}-Unit.xctestplan: Unit tests only (any simulator){ProjectName}-Snapshot.xctestplan: Snapshot tests only (specific simulator required){ProjectName}-ViewInspector.xctestplan: View inspector tests only
Available Projects
Bitwarden: Password Manager appAuthenticator: Authenticator appBitwardenKit: Shared framework
Running Tests from Command Line
Run All Tests
xcodebuild test \
-project Bitwarden.xcodeproj \
-scheme Bitwarden \
-testPlan Bitwarden-Default \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro'
Run Unit Tests Only
xcodebuild test \
-project Bitwarden.xcodeproj \
-scheme Bitwarden \
-testPlan Bitwarden-Unit \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro'
Run Snapshot Tests Only
Important: Must use the specific simulator from configuration files:
# Read the required simulator configuration
DEVICE=$(cat .test-simulator-device-name)
IOS_VERSION=$(cat .test-simulator-ios-version)
xcodebuild test \
-project Bitwarden.xcodeproj \
-scheme Bitwarden \
-testPlan Bitwarden-Snapshot \
-destination "platform=iOS Simulator,name=$DEVICE,OS=$IOS_VERSION"
Run a Specific Test
xcodebuild test \
-project Bitwarden.xcodeproj \
-scheme Bitwarden \
-only-testing:BitwardenShared-Tests/ExampleProcessorTests/test_receive_action_updatesState
Running Tests from Xcode
- Select Test Plan: Product → Scheme → Edit Scheme → Test → Select Test Plan
- Run All Tests: Cmd+U
- Run Specific Test: Click the diamond icon next to the test method
- Run Test Class: Click the diamond icon next to the class name
Simulator Configuration
Unit Tests
Unit tests can run on any simulator thanks to the SKIP_SIMULATOR_CHECK_FOR_TESTS environment variable enabled in all Unit test plans.
Snapshot Tests
Snapshot tests must run on the specific simulator to avoid false failures:
- Device: Defined in .test-simulator-device-name
- iOS Version: Defined in .test-simulator-ios-version
To verify you're using the correct simulator:
cat .test-simulator-device-name # e.g., "iPhone 15 Pro"
cat .test-simulator-ios-version # e.g., "17.0"
Recording New Snapshots
When creating new snapshot tests or updating UI:
-
Run snapshot tests with recording enabled:
# Set environment variable to record new snapshots RECORD_MODE=1 xcodebuild test -testPlan Bitwarden-Snapshot ... -
Or in Xcode, edit the test scheme and add environment variable:
- Key:
RECORD_MODE - Value:
1
- Key:
-
Run the tests to record snapshots
-
Remove the environment variable and run tests again to verify
-
Commit the new snapshot images with your changes
Test Maintenance
When to Update Tests
After Changing Logic
- ✅ Update tests immediately after changing business logic
- ✅ Ensure all affected test cases pass
- ✅ Add new test cases for new branches/edge cases
After Changing UI
- ✅ Update ViewInspector tests if interactions changed
- ✅ Re-record snapshots if visual changes are intentional
- ✅ Verify snapshots in all modes (light, dark, accessibility)
After Refactoring
- ✅ Update test setup if dependencies changed
- ✅ Ensure tests still cover the same scenarios
- ✅ Remove obsolete tests for deleted code
Test Smells to Avoid
❌ Flaky Tests
Symptoms:
- Tests pass sometimes, fail other times
- Tests depend on timing or order
- Tests depend on external state
Solutions:
- Use async/await properly with proper waits
- Mock time-dependent operations
- Reset state in
setUp()andtearDown() - Avoid shared mutable state between tests
❌ Testing Multiple Concerns
Bad: One test validates multiple unrelated things
// Bad: Testing too much in one test
func test_everything() async {
await subject.perform(.loadData)
XCTAssertFalse(subject.state.isLoading)
XCTAssertNotNil(subject.state.data)
subject.receive(.toggleChanged(true))
XCTAssertTrue(subject.state.isToggleOn)
subject.receive(.next)
XCTAssertEqual(coordinator.routes.last, .next)
}
Good: Separate tests for separate concerns
// Good: One test, one concern
func test_perform_loadData_updatesState() async {
await subject.perform(.loadData)
XCTAssertEqual(subject.state.data, expectedData)
}
func test_receive_toggleChanged_updatesToggle() {
subject.receive(.toggleChanged(true))
XCTAssertTrue(subject.state.isToggleOn)
}
func test_receive_next_navigates() {
subject.receive(.next)
XCTAssertEqual(coordinator.routes.last, .next)
}
❌ Not Using Mocks
Bad: Tests that depend on real services/network
// Bad: Using real repository
func test_loadData() async {
let repository = DefaultExampleRepository(/* real dependencies */)
// This makes real API calls!
}
Good: Tests that use mocks for isolation
// Good: Using mocked repository
func test_loadData() async {
mockRepository.loadDataResult = .success(testData)
await subject.perform(.loadData)
XCTAssertEqual(subject.state.data, testData)
}
Test Coverage Guidelines
Aim for High Coverage
- Processors: 100% coverage of actions, effects, state mutations
- Services: 90%+ coverage of public methods
- Repositories: 90%+ coverage of public methods
- Coordinators: 80%+ coverage of routes
- Views: All user interactions tested with ViewInspector
- Models: Test custom logic, computed properties, validation
What NOT to Test
- ❌ Third-party framework internals
- ❌ Apple SDK behaviors
- ❌ Simple getters/setters without logic
- ❌ Trivial computed properties that delegate to another property
Debugging Failing Tests
Step 1: Isolate the Failure
# Run only the failing test
xcodebuild test -only-testing:Target/TestClass/testMethod
Step 2: Check Test Output
- Read the failure message carefully
- Check expected vs actual values
- Look for assertion failures
Step 3: Add Debugging
func test_example() async {
print("State before: \(subject.state)")
await subject.perform(.loadData)
print("State after: \(subject.state)")
print("Mock was called: \(mockService.fetchDataCalled)")
XCTAssertEqual(subject.state.data, expectedData)
}
Step 4: Check Mock Setup
// Verify mock is configured correctly
func test_example() async {
// Explicitly verify mock result is set
XCTAssertNotNil(mockService.fetchDataResult)
await subject.perform(.loadData)
// Verify mock was called
XCTAssertTrue(mockService.fetchDataCalled)
}
Step 5: Snapshot Test Failures
For snapshot test failures:
- Check if you're using the correct simulator
- Review the failure diff images in the test results
- If visual changes are intentional, re-record the snapshot
- If unintentional, fix the UI bug
Continuous Integration
Tests run automatically on CI for:
- ✅ Pull requests to
main - ✅ Commits to
main - ✅ Release branches
CI runs all test plans:
- Unit tests (fast feedback)
- Snapshot tests (visual regression detection)
- View inspector tests (interaction validation)
Ensure all tests pass locally before pushing to prevent CI failures.
Quick Reference for AI Agents
Decision Matrix: Test Type Selection
| Component Type | Unit Tests | ViewInspector | Snapshots |
|---|---|---|---|
| Processor | ✅ Required | ❌ N/A | ❌ N/A |
| Service | ✅ Required | ❌ N/A | ❌ N/A |
| Repository | ✅ Required | ❌ N/A | ❌ N/A |
| Store | ✅ Required | ❌ N/A | ❌ N/A |
| Coordinator | ✅ Required | ❌ N/A | ❌ N/A |
| Model | ✅ If logic | ❌ N/A | ❌ N/A |
| View | ❌ N/A | ✅ Required | ✅ Required |
Common Test Patterns Quick Reference
// 1. Processor Test Template
func test_action_behavior() {
subject.receive(.action)
XCTAssertEqual(subject.state.property, expected)
}
// 2. Async Effect Test Template
func test_effect_behavior() async {
mockService.result = .success(data)
await subject.perform(.effect)
XCTAssertEqual(subject.state.data, data)
}
// 3. Navigation Test Template
func test_action_navigates() {
subject.receive(.action)
XCTAssertEqual(coordinator.routes.last, .expectedRoute)
}
// 4. ViewInspector Test Template
func test_button_sendsAction() throws {
let button = try subject.inspect().find(button: "Title")
try button.tap()
XCTAssertEqual(processor.actions.last, .expectedAction)
}
// 5. Snapshot Test Template
func test_snapshot_mode() {
assertSnapshot(of: subject, as: .defaultPortrait)
}
Test Checklist for AI Agents
When writing tests for a component, ensure:
- Test file named correctly (
ComponentTests.swift) - Test file in same folder as implementation
- Inherits from
BitwardenTestCase setUp()creates fresh instancestearDown()cleans up (sets tonil)- All public methods tested
- Error cases tested
- Edge cases tested
- Mocks used for dependencies
- Assertions are specific (not just "not nil")
- Test names describe behavior (
test_action_outcome) - Tests ordered by function name (see ordering guidelines below)
- Async tests use
async/await - Views tested with ViewInspector AND Snapshots
- Snapshots include light/dark/accessibility modes
Test Ordering Guidelines
Tests should be organized to maximize readability and maintainability:
Ordering Principle
Primary Sort: Alphabetically by the function/method being tested (second part of test name) Secondary Sort: Logically by behavior cluster (not strictly alphabetical)
Test Naming Pattern
test_<functionName>_<behaviorDescription>
- Part 1: Always
test_ - Part 2: The function/method/action being tested (e.g.,
receive,perform,loadData) - Part 3: The behavior being verified (e.g.,
updatesState,showsError,navigates)
Ordering Examples
✅ Correct Ordering:
class ExampleProcessorTests: BitwardenTestCase {
// Tests for loadData() - grouped together, ordered logically
func test_loadData_success_updatesState() async { }
func test_loadData_failure_showsError() async { }
func test_loadData_emptyResponse_setsEmptyState() async { }
// Tests for receive(_:) with different actions - grouped by action
func test_receive_cancelAction_dismissesView() { }
func test_receive_nextAction_navigates() { }
func test_receive_nextAction_whenInvalid_showsError() { }
func test_receive_toggleAction_updatesState() { }
func test_receive_toggleAction_whenDisabled_doesNothing() { }
// Tests for saveData() - grouped together
func test_saveData_success_showsConfirmation() async { }
func test_saveData_failure_showsError() async { }
// Tests for validateInput() - grouped together
func test_validateInput_validEmail_returnsTrue() { }
func test_validateInput_invalidEmail_returnsFalse() { }
}
❌ Incorrect Ordering:
class ExampleProcessorTests: BitwardenTestCase {
// Bad: Tests scattered, not grouped by function
func test_loadData_success_updatesState() async { }
func test_receive_toggleAction_updatesState() { }
func test_saveData_failure_showsError() async { }
func test_loadData_failure_showsError() async { }
// Bad: Strictly alphabetical by behavior ignores logical clustering
func test_receive_cancelAction_dismissesView() { }
func test_receive_nextAction_navigates() { }
func test_receive_toggleAction_updatesState() { }
func test_receive_toggleAction_whenDisabled_doesNothing() { }
func test_receive_nextAction_whenInvalid_showsError() { } // Should be with other nextAction tests
}
Rationale
Why group by function name?
- Related tests stay together
- Easy to find all tests for a specific function
- Makes gaps in test coverage obvious
Why not strict alphabetical on behavior?
- Logical flow is more important (success → failure → edge cases)
- Related behaviors should cluster together
- "Happy path" tests typically come before error cases
Common Grouping Patterns
-
Success → Failure → Edge Cases
func test_fetchData_success_returnsData() async { } func test_fetchData_failure_throwsError() async { } func test_fetchData_emptyResponse_returnsEmptyArray() async { } func test_fetchData_timeout_throwsTimeoutError() async { } -
By Action Type (for receive/perform tests)
// Group all tests for the same action together func test_receive_submitAction_validInput_savesData() { } func test_receive_submitAction_invalidInput_showsError() { } func test_receive_submitAction_emptyInput_showsValidationError() { } -
By State Condition
func test_appear_authenticated_loadsData() async { } func test_appear_unauthenticated_showsLogin() async { } func test_appear_offline_showsOfflineMessage() async { }
Special Cases
Lifecycle Methods: Group together at the top
class ExampleTests: BitwardenTestCase {
// Initialization tests first
func test_init_setsInitialState() { }
func test_init_withParameters_configuresCorrectly() { }
// Then alphabetically by function
func test_loadData_success() async { }
// ...
}
Computed Properties: Group by property name
func test_isValid_whenAllFieldsPopulated_returnsTrue() { }
func test_isValid_whenMissingFields_returnsFalse() { }
Quick Reference for AI Agents
When adding a new test:
- Find the group of tests for the same function/method
- Add the new test within that group
- Order within the group by logical flow (not strict alphabetical)
- If testing a new function, insert the group alphabetically by function name