npx skills add ...
npx skills add bitwarden/android --skill testing-android-code
npx skills add bitwarden/android --skill testing-android-code
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
Benefits:
MainDispatcherExtension for UnconfinedTestDispatcherstateEventFlow() helper for simultaneous StateFlow/EventFlow testingPattern:
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:
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/)
Key patterns: Use FakeDispatcherManager, fixed Clock, and .asSuccess() helpers.
Location: network/src/testFixtures/kotlin/com/bitwarden/network/model/
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| 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 |
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 FakesDeclare 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 fakesclass 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) }
}
}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)
}
}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")// Create results
"data".asSuccess() // Result.success("data")
throwable.asFailure() // Result.failure<T>(throwable)
// Assertions
assertTrue(result.isSuccess)
assertEquals(expectedValue, result.getOrNull())// CORRECT - Call directly, NOT inside runTest
@Test
fun `test exception`() {
assertCoroutineThrows<IllegalStateException> {
repository.throwingFunction()
}
}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