JVM unit tests under sheaf/app/src/test/. Run them with:
cd sheaf && ./gradlew :app:testCI runs ./gradlew :app:test on every push to master before assembling the release APK; failure blocks the build. Test reports are uploaded as the unit-test-reports artifact on each run for inspection.
Current coverage:
util/ErrorMessagesTest—Throwable.toUserMessage()mapping forHttpException(incl. Cloudflare body sniffing) and the IO-exception ladder.data/model/SystemSafetyParsingTest— Moshi parsing ofSystemSafetyResponseandSystemSafetyUpdateResponse. Acts as a regression net for the kind of bug fixed in4ad74c5: a missing@JsonClassannotation causes parsing to fall through to reflection, which in release builds gets stripped by R8 even though debug builds appear fine.
Wired-but-unused dependencies in app/build.gradle.kts: kotlinx-coroutines-test, mockk, turbine. Reach for these when adding ViewModel or repository tests.
These are mapped out but not implemented; pick them up as time and pain warrant.
AltchaSolver— pure crypto/PoW logic, high regression value. Wrinkle: it callsandroid.util.Base64, which throws "not mocked" on JVM. Either stub viamockkStatic(Base64::class), or swap tojava.util.Base64(functionally identical forNO_WRAP).- ViewModels —
MembersViewModel,AuthViewModel,SystemSafetyViewModel, etc. Withmockkfor the repo dependency andturbineforStateFlow/Flowassertions, these are straightforward. - Repositories — once they exist as a thin layer; use a fake
SheafApiServiceand assert mapping/caching behavior.
Renders Compose on the JVM (no emulator) using androidx.compose.ui:ui-test-junit4 + org.robolectric:robolectric + @RunWith(AndroidJUnit4::class) + createAndroidComposeRule. Good for screen-level smoke tests and routing/state assertions. Slower than unit tests (~5–10s startup) but still runnable in CI without an emulator.
Render Compose to PNG on JVM, diff against checked-in goldens. Catches accidental visual regressions that other test layers miss (typography, color, layout). Tradeoff: golden images need updating whenever the design intentionally changes — small maintenance burden, occasionally noisy.
- Paparazzi (Square): standalone, no Robolectric,
@Test+paparazzi.snapshot { … }. - Roborazzi (Robolectric-based): if we already adopt Robolectric for Compose tests, this folds in cheaply.
Real Android runtime, real emulator. The gold standard for end-to-end behavior, but in CI it requires reactivecircus/android-emulator-runner, which is slow (~5 min cold) and prone to flakes on free GitHub runners. Defer until there's a specific concern only an emulator can validate — IPC with Wear, real WorkManager execution, real notification posting, etc.
The bug fixed in 4ad74c5 only manifested in release (R8-obfuscated) builds — unit tests on the debug variant would not have caught it. A useful addition is a minimal instrumented test that exercises the JSON-deserialization paths against an assembleRelease APK. Open question: is the cost (emulator + signed release APK in CI) worth it vs. keeping a strict proguard-rules.pro review discipline. Revisit after we've shipped a few more releases.
- No mutation testing.
- No coverage gates. (Add
koverif/when coverage thresholds become useful — premature today with two test files.) - No fuzzing of API model parsers.