skills/bitwarden/android/testing-android-code

testing-android-code

SKILL.md

Testing Android Code - Bitwarden Testing Patterns

This skill provides tactical testing guidance for Bitwarden-specific patterns. For comprehensive architecture and testing philosophy, consult docs/ARCHITECTURE.md.

Test Framework Configuration

Required Dependencies:

  • JUnit 5 (jupiter), MockK, Turbine (app.cash.turbine)
  • kotlinx.coroutines.test, Robolectric, Compose Test

Critical Note: Tests run with en-US locale for consistency. Don't assume other locales.


A. ViewModel Testing Patterns

Base Class: BaseViewModelTest

Always extend BaseViewModelTest for ViewModel tests.

Location: ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseViewModelTest.kt

Benefits:

  • Automatically registers MainDispatcherExtension for UnconfinedTestDispatcher
  • Provides stateEventFlow() helper for simultaneous StateFlow/EventFlow testing

Pattern:

class ExampleViewModelTest : BaseViewModelTest() {
    private val mockRepository: ExampleRepository = mockk()
    private val savedStateHandle = SavedStateHandle(mapOf(KEY_STATE to INITIAL_STATE))

    @Test
    fun `ButtonClick should fetch data and update state`() = runTest {
        coEvery { mockRepository.fetchData(any()) } returns Result.success("data")

        val viewModel = ExampleViewModel(savedStateHandle, mockRepository)

        viewModel.stateFlow.test {
            assertEquals(INITIAL_STATE, awaitItem())
            viewModel.trySendAction(ExampleAction.ButtonClick)
            assertEquals(INITIAL_STATE.copy(data = "data"), awaitItem())
        }

        coVerify { mockRepository.fetchData(any()) }
    }
}

For complete examples: See references/test-base-classes.md

StateFlow vs EventFlow (Critical Distinction)

Flow Type Replay First Action Pattern
StateFlow Yes (1) awaitItem() gets current state Expect initial → trigger → expect new
EventFlow No expectNoEvents() first expectNoEvents → trigger → expect event

For detailed patterns: See references/flow-testing-patterns.md


B. Compose UI Testing Patterns

Base Class: BitwardenComposeTest

Always extend BitwardenComposeTest for Compose screen tests.

Location: app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt

Benefits:

  • Pre-configures all Bitwarden managers (FeatureFlags, AuthTab, Biometrics, etc.)
  • Wraps content in BitwardenTheme and LocalManagerProvider
  • Provides fixed Clock for deterministic time-based tests

Pattern:

class ExampleScreenTest : BitwardenComposeTest() {
    private var haveCalledNavigateBack = false
    private val mutableEventFlow = bufferedMutableSharedFlow<ExampleEvent>()
    private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
    private val viewModel = mockk<ExampleViewModel>(relaxed = true) {
        every { eventFlow } returns mutableEventFlow
        every { stateFlow } returns mutableStateFlow
    }

    @Before
    fun setup() {
        setContent {
            ExampleScreen(
                onNavigateBack = { haveCalledNavigateBack = true },
                viewModel = viewModel,
            )
        }
    }

    @Test
    fun `on back click should send BackClick action`() {
        composeTestRule.onNodeWithContentDescription("Back").performClick()
        verify { viewModel.trySendAction(ExampleAction.BackClick) }
    }
}

Note: Use bufferedMutableSharedFlow for event testing in Compose tests. Default replay is 0; pass replay = 1 if needed.

For complete base class details: See references/test-base-classes.md


C. Repository and Service Testing

Service Testing with MockWebServer

Base Class: BaseServiceTest (network/src/testFixtures/)

class ExampleServiceTest : BaseServiceTest() {
    private val api: ExampleApi = retrofit.create()
    private val service = ExampleServiceImpl(api)

    @Test
    fun `getConfig should return success when API succeeds`() = runTest {
        server.enqueue(MockResponse().setBody(EXPECTED_JSON))
        val result = service.getConfig()
        assertEquals(EXPECTED_RESPONSE.asSuccess(), result)
    }
}

Repository Testing Pattern

class ExampleRepositoryTest {
    private val fixedClock: Clock = Clock.fixed(
        Instant.parse("2023-10-27T12:00:00Z"),
        ZoneOffset.UTC,
    )
    private val dispatcherManager = FakeDispatcherManager()
    private val mockDiskSource: ExampleDiskSource = mockk()
    private val mockService: ExampleService = mockk()

    private val repository = ExampleRepositoryImpl(
        clock = fixedClock,
        exampleDiskSource = mockDiskSource,
        exampleService = mockService,
        dispatcherManager = dispatcherManager,
    )

    @Test
    fun `fetchData should return success when service succeeds`() = runTest {
        coEvery { mockService.getData(any()) } returns expectedData.asSuccess()
        val result = repository.fetchData(userId)
        assertTrue(result.isSuccess)
    }
}

Key patterns: Use FakeDispatcherManager, fixed Clock, and .asSuccess() helpers.


D. Test Data Builders

Builder Pattern with Number Parameter

Location: network/src/testFixtures/kotlin/com/bitwarden/network/model/

fun createMockCipher(
    number: Int,
    id: String = "mockId-$number",
    name: String? = "mockName-$number",
    // ... more parameters with defaults
): SyncResponseJson.Cipher

// Usage:
val cipher1 = createMockCipher(number = 1)  // mockId-1, mockName-1
val cipher2 = createMockCipher(number = 2)  // mockId-2, mockName-2
val custom = createMockCipher(number = 3, name = "Custom")

Available Builders (35+):

  • Cipher: createMockCipher(), createMockLogin(), createMockCard(), createMockIdentity(), createMockSecureNote(), createMockSshKey(), createMockField(), createMockUri(), createMockFido2Credential(), createMockPasswordHistory(), createMockCipherPermissions()
  • Sync: createMockSyncResponse(), createMockFolder(), createMockCollection(), createMockPolicy(), createMockDomains()
  • Send: createMockSend(), createMockFile(), createMockText(), createMockSendJsonRequest()
  • Profile: createMockProfile(), createMockOrganization(), createMockProvider(), createMockPermissions()
  • Attachments: createMockAttachment(), createMockAttachmentJsonRequest(), createMockAttachmentResponse()

See network/src/testFixtures/kotlin/com/bitwarden/network/model/ for full list.


E. Result Type Testing

Locations:

  • .asSuccess(), .asFailure(): core/src/main/kotlin/com/bitwarden/core/data/util/ResultExtensions.kt
  • assertCoroutineThrows: core/src/testFixtures/kotlin/com/bitwarden/core/data/util/TestHelpers.kt
// Create results
"data".asSuccess()              // Result.success("data")
throwable.asFailure()           // Result.failure<T>(throwable)

// Assertions
assertTrue(result.isSuccess)
assertEquals(expectedValue, result.getOrNull())

F. Test Utilities and Helpers

Fake Implementations

Fake Location Purpose
FakeDispatcherManager core/src/testFixtures/ Deterministic coroutine execution
FakeConfigDiskSource data/src/testFixtures/ In-memory config storage
FakeSharedPreferences data/src/testFixtures/ Memory-backed SharedPreferences

Exception Testing (CRITICAL)

// CORRECT - Call directly, NOT inside runTest
@Test
fun `test exception`() {
    assertCoroutineThrows<IllegalStateException> {
        repository.throwingFunction()
    }
}

Why: runTest catches exceptions and rethrows them, breaking the assertion pattern.


G. Critical Gotchas

Common testing mistakes in Bitwarden. For complete details and examples: See references/critical-gotchas.md

Core Patterns:

  • assertCoroutineThrows + runTest - Never wrap in runTest; call directly
  • Static mock cleanup - Always unmockkStatic() in @After
  • StateFlow vs EventFlow - StateFlow: awaitItem() first; EventFlow: expectNoEvents() first
  • FakeDispatcherManager - Always use instead of real DispatcherManagerImpl
  • Coroutine test wrapper - Use runTest { } for all Flow/coroutine tests

Assertion Patterns:

  • Complete state assertions - Assert entire state objects, not individual fields
  • JUnit over Kotlin - Use assertTrue(), not Kotlin's assert()
  • Use Result extensions - Use asSuccess() and asFailure() for Result type assertions

Test Design:

  • Fake vs Mock strategy - Use Fakes for happy paths, Mocks for error paths
  • DI over static mocking - Extract interfaces (like UuidManager) instead of mockkStatic
  • Null stream testing - Test null returns from ContentResolver operations
  • bufferedMutableSharedFlow - Use with .onSubscription { emit(state) } in Fakes
  • Test factory methods - Accept domain state types, not SavedStateHandle

H. Test File Organization

Directory Structure

module/src/test/kotlin/com/bitwarden/.../
├── ui/*ScreenTest.kt, *ViewModelTest.kt
├── data/repository/*RepositoryTest.kt
└── network/service/*ServiceTest.kt

module/src/testFixtures/kotlin/com/bitwarden/.../
├── util/TestHelpers.kt
├── base/Base*Test.kt
└── model/*Util.kt

Test Naming

  • Classes: *Test.kt, *ScreenTest.kt, *ViewModelTest.kt
  • Functions: `given state when action should result`

Summary

Key Bitwarden-specific testing patterns:

  1. BaseViewModelTest - Automatic dispatcher setup with stateEventFlow() helper
  2. BitwardenComposeTest - Pre-configured with all managers and theme
  3. BaseServiceTest - MockWebServer setup for network testing
  4. Turbine Flow Testing - StateFlow (replay) vs EventFlow (no replay)
  5. Test Data Builders - Consistent number: Int parameter pattern
  6. Fake Implementations - FakeDispatcherManager, FakeConfigDiskSource
  7. Result Type Testing - .asSuccess(), .asFailure()

Always consult: docs/ARCHITECTURE.md and existing test files for reference implementations.


Reference Documentation

For detailed information, see:

  • references/test-base-classes.md - Detailed base class documentation and usage patterns
  • references/flow-testing-patterns.md - Complete Turbine patterns for StateFlow/EventFlow
  • references/critical-gotchas.md - Full anti-pattern reference and debugging tips

Complete Examples:

  • examples/viewmodel-test-example.md - Full ViewModel test with StateFlow/EventFlow
  • examples/compose-screen-test-example.md - Full Compose screen test
  • examples/repository-test-example.md - Full repository test with mocks and fakes
Weekly Installs
36
GitHub Stars
8.6K
First Seen
Feb 14, 2026
Installed on
gemini-cli36
github-copilot36
codex36
opencode36
amp35
kimi-cli35