This skill should be used when writing or reviewing tests for Android code in Bitwarden. Triggered by "BaseViewModelTest", "BitwardenComposeTest", "BaseServiceTest", "stateEventFlow", "bufferedMutableSharedFlow", "FakeDispatcherManager", "expectNoEvents", "assertCoroutineThrows", "createMockCipher", "createMockSend", "asSuccess", "Why is my Bitwarden test failing?", or testing questions about ViewModels, repositories, Compose screens, or data sources 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
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
⛔ STOP —
@Suppress("MaxLineLength"): Do NOT add this annotation unless thefundeclaration line actually exceeds 100 characters. Count the characters first. Do not copy it from nearby tests. Detekt will tell you if it's needed — when in doubt, leave it off.
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