mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 11:16:12 -04:00
Replaces capod's older billing core, upgrade UI and their tests with the canonical sdmaid-se stack at the pinned revision. Core (gplay): BillingManager/BillingConnection/BillingConnectionProvider on billing 8.3 with the centralized connect loop, merging purchases-listener overlay and the canonical ack pipeline; the dying ack collector, the ackedTokens gate and the in-billing foreground loop are gone. Full canonical exception set (internal/network/offer-unavailable added), OurSku with capod's product ids, BillingCache with snapshot()/episode-guarded stampLastProState. FOSS: UpgradeControlFoss becomes UpgradeRepoFoss and exposes the canonical API surface over capod's RETAINED FossUpgrade/FossCache schema — existing supporter records must keep decoding. Diagnostics: UpgradeDiagnostics + gplay/foss implementations, read by RecorderModule next to CurriculumVitae's Pro history as two independent, isolated header reads. UI: canonical upgrade screens for both flavors under common/upgrade/ui with capod chrome (M3 AlertDialog keeping rotation-safety, capod Scaffold, capod previews). Nav.Main.Upgrade gains `forced`. Entitlement refresh moves to a per-resume, unthrottled MainActivity call. Strings reuse capod's existing translated ids wherever equivalent; only referenced-but-missing ones are authored. mockk 1.12.4 -> 1.14.9: 1.12.4 cannot synthesize a sealed-class return value while recording, which the ported restore tests need.
80 lines
3.5 KiB
Markdown
80 lines
3.5 KiB
Markdown
---
|
|
description: Unit test conventions — JUnit 5, kotest assertions, mockk, BaseTest, and which Gradle task runs which source set
|
|
paths:
|
|
- "app/src/test/**"
|
|
- "app/src/testFoss/**"
|
|
- "app/src/testGplay/**"
|
|
- "app/build.gradle.kts"
|
|
- "buildSrc/src/main/java/Dependencies.kt"
|
|
---
|
|
|
|
# Testing
|
|
|
|
The stack here is not the Android default — check this before reaching for a familiar library.
|
|
|
|
## Libraries
|
|
|
|
- **JUnit 5** (`org.junit.jupiter.api.Test`). Gradle sets `useJUnitPlatform()`.
|
|
- **kotest** for assertions: `io.kotest.matchers.shouldBe`, `shouldBeNull`, `shouldBeInstanceOf`,
|
|
`shouldContainExactly`, `io.kotest.assertions.throwables.shouldThrow`. Use kotest for new
|
|
assertions — `MediaControlTest` still uses JUnit `Assertions.*` and is a legacy exception.
|
|
- **mockk** for mocking. Not Mockito.
|
|
- **Turbine is not a dependency.** `testhelpers.flow.FlowTest` provides a `Flow<T>.test()` helper —
|
|
use it rather than adding one.
|
|
|
|
## Base classes
|
|
|
|
Extend `testhelpers.BaseTest`, or the applicable specialized base that already extends it:
|
|
|
|
- `BaseBlePodsTest` — BLE advertisement parsing per pod model
|
|
- `BaseAapSessionTest` — AAP protocol/session tests
|
|
|
|
`BaseTest` installs a `JUnitLogger` and calls `unmockkAll()` in `@AfterAll`. Skipping it can leave
|
|
global mockk and logging state behind for later test classes.
|
|
|
|
The only exceptions are the Robolectric-backed tests (Compose UI via
|
|
`testhelpers.compose.BaseComposeRobolectricTest`, and the few DataStore-backed ones such as
|
|
`CurriculumVitaeProHistoryTest`), which use JUnit 4 `@RunWith`/`@Rule` via `junit-vintage-engine`.
|
|
Don't copy that pattern for a plain unit test.
|
|
|
|
## Source sets and Gradle tasks
|
|
|
|
Each task compiles and runs only its own flavor — running the wrong one silently skips your test.
|
|
|
|
| Test location | Task |
|
|
|---|---|
|
|
| `app/src/test/` (shared) | either; run both before pushing |
|
|
| `app/src/testFoss/` | `./gradlew testFossDebugUnitTest` |
|
|
| `app/src/testGplay/` | `./gradlew testGplayDebugUnitTest` |
|
|
|
|
CI runs both. Flavor-specific tests are for code that only exists in that flavor — billing in
|
|
`gplay`, the sponsor-based upgrade flow in `foss`.
|
|
|
|
## Helpers that already exist
|
|
|
|
- `runTest2(autoCancel, context, expectedError, testBody)` in `testhelpers/coroutine/TestExtensions.kt` —
|
|
use `expectedError = SomeException::class` instead of hand-rolling a throws-assertion around `runTest`
|
|
- `FakeDataStoreValue<T>(initial)` in `testhelpers/datastore/` — a working fake with a real backing
|
|
`MutableStateFlow`; read/write it through `.value` and pass `.mock` to the code under test
|
|
|
|
## Mocking `DataStoreValue`
|
|
|
|
`DataStoreValue.value()` and `.value(T)` are **extension functions** (`DataStoreValue.kt:54,56`), not
|
|
members, so MockK cannot stub them. They delegate to `flow.first()` and `update { }` — stub those:
|
|
|
|
```kotlin
|
|
every { someSetting.flow } returns flowOf(value) // covers .value() reads
|
|
coVerify { someSetting.update(any()) } // verifies .value(x) writes
|
|
```
|
|
|
|
`UpgradeRepoGplayTest` uses this shape. Prefer `FakeDataStoreValue` when you need reads and writes to
|
|
actually round-trip.
|
|
|
|
## Reading ViewModel state
|
|
|
|
`ViewModel2.asLiveState()` is `stateIn(..., initialValue = null).filterNotNull()` with
|
|
`SharingStarted.WhileSubscribed(5_000)` — so `vm.state` is a `Flow`, not a `StateFlow`, and has no
|
|
`.value` to read. Collect it: `vm.state.first()` is the established pattern across the existing
|
|
ViewModel tests. Because the upstream only runs while subscribed, a test that never collects sees
|
|
nothing happen at all.
|