This skill should be used when writing or reviewing tests for Android code in Bitwarden...
This skill provides tactical testing guidance for Bitwarden-specific patterns. For comprehensive architecture and testing philosophy, consult docs/ARCHITECTURE.md.
Required Dependencies:
Critical Note: Tests run with en-US locale for consistency. Don't assume other locales.
Always extend BaseViewModelTest for ViewModel tests.
Location: ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseViewModelTest.kt
Benefits:
MainDispatcherExtension for UnconfinedTestDispatcherstateEventFlow() helper for simultaneous StateFlow/EventFlow testingPattern:
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
| 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
Always extend BitwardenComposeTest for Compose screen tests.
Location: app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt
Benefits:
BitwardenTheme and LocalManagerProviderPattern:
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
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)
}
}
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.
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+):
createMockCipher(), createMockLogin(), createMockCard(), createMockIdentity(), createMockSecureNote(), createMockSshKey(), createMockField(), createMockUri(), createMockFido2Credential(), createMockPasswordHistory(), createMockCipherPermissions()createMockSyncResponse(), createMockFolder(), createMockCollection(), createMockPolicy(), createMockDomains()createMockSend(), createMockFile(), createMockText(), createMockSendJsonRequest()createMockProfile(), createMockOrganization(), createMockProvider(), createMockPermissions()createMockAttachment(), createMockAttachmentJsonRequest(), createMockAttachmentResponse()See network/src/testFixtures/kotlin/com/bitwarden/network/model/ for full list.
Locations:
.asSuccess(), .asFailure(): core/src/main/kotlin/com/bitwarden/core/data/util/ResultExtensions.ktassertCoroutineThrows: 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())
| 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 |
// 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.
Common testing mistakes in Bitwarden. For complete details and examples: See references/critical-gotchas.md
Core Patterns:
runTest; call directlyunmockkStatic() in @AfterawaitItem() first; EventFlow: expectNoEvents() firstDispatcherManagerImplrunTest { } for all Flow/coroutine testsAssertion Patterns:
assertTrue(), not Kotlin's assert()asSuccess() and asFailure() for Result type assertionsTest Design:
.onSubscription { emit(state) } in Fakesmodule/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
Declare test constants as top-level private const val at the bottom of the file, after the class closing brace. Do NOT use companion object for test constants.
*Test.kt, *ScreenTest.kt, *ViewModelTest.kt`given state when action should result`Key Bitwarden-specific testing patterns:
stateEventFlow() helpernumber: Int parameter pattern.asSuccess(), .asFailure()Always consult: docs/ARCHITECTURE.md and existing test files for reference implementations.
For detailed information, see:
references/test-base-classes.md - Detailed base class documentation and usage patternsreferences/flow-testing-patterns.md - Complete Turbine patterns for StateFlow/EventFlowreferences/critical-gotchas.md - Full anti-pattern reference and debugging tipsComplete Examples:
examples/viewmodel-test-example.md - Full ViewModel test with StateFlow/EventFlowexamples/compose-screen-test-example.md - Full Compose screen testexamples/repository-test-example.md - Full repository test with mocks and fakes