Skip to content

Commit 5dc2157

Browse files
authored
Merge branch 'main' into claude/test-bitwarden-init-plugin
2 parents 94b76e3 + 2e18b07 commit 5dc2157

File tree

300 files changed

+17930
-1890
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

300 files changed

+17930
-1890
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Testing Android Code Skill
2+
3+
Quick-reference guide for writing and reviewing tests in the Bitwarden Android codebase.
4+
5+
## Purpose
6+
7+
This skill provides tactical testing guidance for Bitwarden-specific patterns. It focuses on base test classes, test utilities, and common gotchas unique to this codebase rather than general testing concepts.
8+
9+
## When This Skill Activates
10+
11+
The skill automatically loads when you ask questions like:
12+
13+
- "How do I test this ViewModel?"
14+
- "Why is my Bitwarden test failing?"
15+
- "Write tests for this repository"
16+
17+
Or when you mention terms like: `BaseViewModelTest`, `BitwardenComposeTest`, `stateEventFlow`, `bufferedMutableSharedFlow`, `FakeDispatcherManager`, `createMockCipher`, `asSuccess`
18+
19+
## What's Included
20+
21+
| File | Purpose |
22+
|------|---------|
23+
| `SKILL.md` | Core testing patterns and base class locations |
24+
| `references/test-base-classes.md` | Detailed base class documentation |
25+
| `references/flow-testing-patterns.md` | Turbine patterns for StateFlow/EventFlow |
26+
| `references/critical-gotchas.md` | Anti-patterns and debugging tips |
27+
| `examples/viewmodel-test-example.md` | Complete ViewModel test example |
28+
| `examples/compose-screen-test-example.md` | Complete Compose screen test |
29+
| `examples/repository-test-example.md` | Complete repository test with mocks |
30+
31+
## Patterns Covered
32+
33+
1. **BaseViewModelTest** - Automatic dispatcher setup with `stateEventFlow()` helper
34+
2. **BitwardenComposeTest** - Pre-configured with all managers and theme
35+
3. **BaseServiceTest** - MockWebServer setup for network testing
36+
4. **Turbine Flow Testing** - StateFlow (replay) vs EventFlow (no replay)
37+
5. **Test Data Builders** - 35+ `createMock*` functions with `number: Int` pattern
38+
6. **Fake Implementations** - FakeDispatcherManager, FakeConfigDiskSource
39+
7. **Result Type Testing** - `.asSuccess()`, `.asFailure()`, `assertCoroutineThrows`
40+
41+
## Quick Start
42+
43+
For comprehensive architecture and testing philosophy, see:
44+
- `docs/ARCHITECTURE.md`
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
---
2+
name: testing-android-code
3+
description: 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.
4+
version: 1.0.0
5+
---
6+
7+
# Testing Android Code - Bitwarden Testing Patterns
8+
9+
**This skill provides tactical testing guidance for Bitwarden-specific patterns.** For comprehensive architecture and testing philosophy, consult `docs/ARCHITECTURE.md`.
10+
11+
## Test Framework Configuration
12+
13+
**Required Dependencies:**
14+
- **JUnit 5** (jupiter), **MockK**, **Turbine** (app.cash.turbine)
15+
- **kotlinx.coroutines.test**, **Robolectric**, **Compose Test**
16+
17+
**Critical Note:** Tests run with en-US locale for consistency. Don't assume other locales.
18+
19+
---
20+
21+
## A. ViewModel Testing Patterns
22+
23+
### Base Class: BaseViewModelTest
24+
25+
**Always extend `BaseViewModelTest` for ViewModel tests.**
26+
27+
**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseViewModelTest.kt`
28+
29+
**Benefits:**
30+
- Automatically registers `MainDispatcherExtension` for `UnconfinedTestDispatcher`
31+
- Provides `stateEventFlow()` helper for simultaneous StateFlow/EventFlow testing
32+
33+
**Pattern:**
34+
```kotlin
35+
class ExampleViewModelTest : BaseViewModelTest() {
36+
private val mockRepository: ExampleRepository = mockk()
37+
private val savedStateHandle = SavedStateHandle(mapOf(KEY_STATE to INITIAL_STATE))
38+
39+
@Test
40+
fun `ButtonClick should fetch data and update state`() = runTest {
41+
coEvery { mockRepository.fetchData(any()) } returns Result.success("data")
42+
43+
val viewModel = ExampleViewModel(savedStateHandle, mockRepository)
44+
45+
viewModel.stateFlow.test {
46+
assertEquals(INITIAL_STATE, awaitItem())
47+
viewModel.trySendAction(ExampleAction.ButtonClick)
48+
assertEquals(INITIAL_STATE.copy(data = "data"), awaitItem())
49+
}
50+
51+
coVerify { mockRepository.fetchData(any()) }
52+
}
53+
}
54+
```
55+
56+
**For complete examples:** See `references/test-base-classes.md`
57+
58+
### StateFlow vs EventFlow (Critical Distinction)
59+
60+
| Flow Type | Replay | First Action | Pattern |
61+
|-----------|--------|--------------|---------|
62+
| StateFlow | Yes (1) | `awaitItem()` gets current state | Expect initial → trigger → expect new |
63+
| EventFlow | No | `expectNoEvents()` first | expectNoEvents → trigger → expect event |
64+
65+
**For detailed patterns:** See `references/flow-testing-patterns.md`
66+
67+
---
68+
69+
## B. Compose UI Testing Patterns
70+
71+
### Base Class: BitwardenComposeTest
72+
73+
**Always extend `BitwardenComposeTest` for Compose screen tests.**
74+
75+
**Location:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt`
76+
77+
**Benefits:**
78+
- Pre-configures all Bitwarden managers (FeatureFlags, AuthTab, Biometrics, etc.)
79+
- Wraps content in `BitwardenTheme` and `LocalManagerProvider`
80+
- Provides fixed Clock for deterministic time-based tests
81+
82+
**Pattern:**
83+
```kotlin
84+
class ExampleScreenTest : BitwardenComposeTest() {
85+
private var haveCalledNavigateBack = false
86+
private val mutableEventFlow = bufferedMutableSharedFlow<ExampleEvent>()
87+
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
88+
private val viewModel = mockk<ExampleViewModel>(relaxed = true) {
89+
every { eventFlow } returns mutableEventFlow
90+
every { stateFlow } returns mutableStateFlow
91+
}
92+
93+
@Before
94+
fun setup() {
95+
setContent {
96+
ExampleScreen(
97+
onNavigateBack = { haveCalledNavigateBack = true },
98+
viewModel = viewModel,
99+
)
100+
}
101+
}
102+
103+
@Test
104+
fun `on back click should send BackClick action`() {
105+
composeTestRule.onNodeWithContentDescription("Back").performClick()
106+
verify { viewModel.trySendAction(ExampleAction.BackClick) }
107+
}
108+
}
109+
```
110+
111+
**Note:** Use `bufferedMutableSharedFlow` for event testing in Compose tests. Default replay is 0; pass `replay = 1` if needed.
112+
113+
**For complete base class details:** See `references/test-base-classes.md`
114+
115+
---
116+
117+
## C. Repository and Service Testing
118+
119+
### Service Testing with MockWebServer
120+
121+
**Base Class:** `BaseServiceTest` (`network/src/testFixtures/`)
122+
123+
```kotlin
124+
class ExampleServiceTest : BaseServiceTest() {
125+
private val api: ExampleApi = retrofit.create()
126+
private val service = ExampleServiceImpl(api)
127+
128+
@Test
129+
fun `getConfig should return success when API succeeds`() = runTest {
130+
server.enqueue(MockResponse().setBody(EXPECTED_JSON))
131+
val result = service.getConfig()
132+
assertEquals(EXPECTED_RESPONSE.asSuccess(), result)
133+
}
134+
}
135+
```
136+
137+
### Repository Testing Pattern
138+
139+
```kotlin
140+
class ExampleRepositoryTest {
141+
private val fixedClock: Clock = Clock.fixed(
142+
Instant.parse("2023-10-27T12:00:00Z"),
143+
ZoneOffset.UTC,
144+
)
145+
private val dispatcherManager = FakeDispatcherManager()
146+
private val mockDiskSource: ExampleDiskSource = mockk()
147+
private val mockService: ExampleService = mockk()
148+
149+
private val repository = ExampleRepositoryImpl(
150+
clock = fixedClock,
151+
exampleDiskSource = mockDiskSource,
152+
exampleService = mockService,
153+
dispatcherManager = dispatcherManager,
154+
)
155+
156+
@Test
157+
fun `fetchData should return success when service succeeds`() = runTest {
158+
coEvery { mockService.getData(any()) } returns expectedData.asSuccess()
159+
val result = repository.fetchData(userId)
160+
assertTrue(result.isSuccess)
161+
}
162+
}
163+
```
164+
165+
**Key patterns:** Use `FakeDispatcherManager`, fixed Clock, and `.asSuccess()` helpers.
166+
167+
---
168+
169+
## D. Test Data Builders
170+
171+
### Builder Pattern with Number Parameter
172+
173+
**Location:** `network/src/testFixtures/kotlin/com/bitwarden/network/model/`
174+
175+
```kotlin
176+
fun createMockCipher(
177+
number: Int,
178+
id: String = "mockId-$number",
179+
name: String? = "mockName-$number",
180+
// ... more parameters with defaults
181+
): SyncResponseJson.Cipher
182+
183+
// Usage:
184+
val cipher1 = createMockCipher(number = 1) // mockId-1, mockName-1
185+
val cipher2 = createMockCipher(number = 2) // mockId-2, mockName-2
186+
val custom = createMockCipher(number = 3, name = "Custom")
187+
```
188+
189+
**Available Builders (35+):**
190+
- **Cipher:** `createMockCipher()`, `createMockLogin()`, `createMockCard()`, `createMockIdentity()`, `createMockSecureNote()`, `createMockSshKey()`, `createMockField()`, `createMockUri()`, `createMockFido2Credential()`, `createMockPasswordHistory()`, `createMockCipherPermissions()`
191+
- **Sync:** `createMockSyncResponse()`, `createMockFolder()`, `createMockCollection()`, `createMockPolicy()`, `createMockDomains()`
192+
- **Send:** `createMockSend()`, `createMockFile()`, `createMockText()`, `createMockSendJsonRequest()`
193+
- **Profile:** `createMockProfile()`, `createMockOrganization()`, `createMockProvider()`, `createMockPermissions()`
194+
- **Attachments:** `createMockAttachment()`, `createMockAttachmentJsonRequest()`, `createMockAttachmentResponse()`
195+
196+
See `network/src/testFixtures/kotlin/com/bitwarden/network/model/` for full list.
197+
198+
---
199+
200+
## E. Result Type Testing
201+
202+
**Locations:**
203+
- `.asSuccess()`, `.asFailure()`: `core/src/main/kotlin/com/bitwarden/core/data/util/ResultExtensions.kt`
204+
- `assertCoroutineThrows`: `core/src/testFixtures/kotlin/com/bitwarden/core/data/util/TestHelpers.kt`
205+
206+
```kotlin
207+
// Create results
208+
"data".asSuccess() // Result.success("data")
209+
throwable.asFailure() // Result.failure<T>(throwable)
210+
211+
// Assertions
212+
assertTrue(result.isSuccess)
213+
assertEquals(expectedValue, result.getOrNull())
214+
```
215+
216+
---
217+
218+
## F. Test Utilities and Helpers
219+
220+
### Fake Implementations
221+
222+
| Fake | Location | Purpose |
223+
|------|----------|---------|
224+
| `FakeDispatcherManager` | `core/src/testFixtures/` | Deterministic coroutine execution |
225+
| `FakeConfigDiskSource` | `data/src/testFixtures/` | In-memory config storage |
226+
| `FakeSharedPreferences` | `data/src/testFixtures/` | Memory-backed SharedPreferences |
227+
228+
### Exception Testing (CRITICAL)
229+
230+
```kotlin
231+
// CORRECT - Call directly, NOT inside runTest
232+
@Test
233+
fun `test exception`() {
234+
assertCoroutineThrows<IllegalStateException> {
235+
repository.throwingFunction()
236+
}
237+
}
238+
```
239+
240+
**Why:** `runTest` catches exceptions and rethrows them, breaking the assertion pattern.
241+
242+
---
243+
244+
## G. Critical Gotchas
245+
246+
Common testing mistakes in Bitwarden. **For complete details and examples:** See `references/critical-gotchas.md`
247+
248+
**Core Patterns:**
249+
- **assertCoroutineThrows + runTest** - Never wrap in `runTest`; call directly
250+
- **Static mock cleanup** - Always `unmockkStatic()` in `@After`
251+
- **StateFlow vs EventFlow** - StateFlow: `awaitItem()` first; EventFlow: `expectNoEvents()` first
252+
- **FakeDispatcherManager** - Always use instead of real `DispatcherManagerImpl`
253+
- **Coroutine test wrapper** - Use `runTest { }` for all Flow/coroutine tests
254+
255+
**Assertion Patterns:**
256+
- **Complete state assertions** - Assert entire state objects, not individual fields
257+
- **JUnit over Kotlin** - Use `assertTrue()`, not Kotlin's `assert()`
258+
- **Use Result extensions** - Use `asSuccess()` and `asFailure()` for Result type assertions
259+
260+
**Test Design:**
261+
- **Fake vs Mock strategy** - Use Fakes for happy paths, Mocks for error paths
262+
- **DI over static mocking** - Extract interfaces (like UuidManager) instead of mockkStatic
263+
- **Null stream testing** - Test null returns from ContentResolver operations
264+
- **bufferedMutableSharedFlow** - Use with `.onSubscription { emit(state) }` in Fakes
265+
- **Test factory methods** - Accept domain state types, not SavedStateHandle
266+
267+
---
268+
269+
## H. Test File Organization
270+
271+
### Directory Structure
272+
273+
```
274+
module/src/test/kotlin/com/bitwarden/.../
275+
├── ui/*ScreenTest.kt, *ViewModelTest.kt
276+
├── data/repository/*RepositoryTest.kt
277+
└── network/service/*ServiceTest.kt
278+
279+
module/src/testFixtures/kotlin/com/bitwarden/.../
280+
├── util/TestHelpers.kt
281+
├── base/Base*Test.kt
282+
└── model/*Util.kt
283+
```
284+
285+
### Test Naming
286+
287+
- Classes: `*Test.kt`, `*ScreenTest.kt`, `*ViewModelTest.kt`
288+
- Functions: `` `given state when action should result` ``
289+
290+
---
291+
292+
## Summary
293+
294+
Key Bitwarden-specific testing patterns:
295+
296+
1. **BaseViewModelTest** - Automatic dispatcher setup with `stateEventFlow()` helper
297+
2. **BitwardenComposeTest** - Pre-configured with all managers and theme
298+
3. **BaseServiceTest** - MockWebServer setup for network testing
299+
4. **Turbine Flow Testing** - StateFlow (replay) vs EventFlow (no replay)
300+
5. **Test Data Builders** - Consistent `number: Int` parameter pattern
301+
6. **Fake Implementations** - FakeDispatcherManager, FakeConfigDiskSource
302+
7. **Result Type Testing** - `.asSuccess()`, `.asFailure()`
303+
304+
**Always consult:** `docs/ARCHITECTURE.md` and existing test files for reference implementations.
305+
306+
---
307+
308+
## Reference Documentation
309+
310+
For detailed information, see:
311+
312+
- `references/test-base-classes.md` - Detailed base class documentation and usage patterns
313+
- `references/flow-testing-patterns.md` - Complete Turbine patterns for StateFlow/EventFlow
314+
- `references/critical-gotchas.md` - Full anti-pattern reference and debugging tips
315+
316+
**Complete Examples:**
317+
- `examples/viewmodel-test-example.md` - Full ViewModel test with StateFlow/EventFlow
318+
- `examples/compose-screen-test-example.md` - Full Compose screen test
319+
- `examples/repository-test-example.md` - Full repository test with mocks and fakes

0 commit comments

Comments
 (0)