Testing
~280 test files covering ViewModels, mappers, repos; UI test coverage moderate.
Executive summary
Costco Android has a mature, multi-layered test stack: ~170 unit-test files, ~80 instrumentation tests across 30+ modules. The unit-test foundation is JUnit 4 + MockK + Turbine + a custom MainCoroutineRule; UI automation uses Compose UI Test + Espresso + UiAutomator with Karumi Shot for screenshot regression. JaCoCo enforces a 40.81% coverage threshold, which is low for a retail-scale codebase. There are concrete gaps around library version drift (Hilt-testing 2.28-alpha vs Hilt 2.56), absent Macrobenchmark / baseline profiles, and no Firebase Test Lab integration in CI.
1. Tooling inventory
All versions sourced from gradle/libs.versions.toml and the Costco/build.gradle file.
| Layer | Tool | Version | Purpose |
|---|---|---|---|
| Unit testing | JUnit 4 | 4.13.2 | Primary test runner across all modules |
| JUnit 5 (Jupiter) | 6.0.3 | Available but not adopted broadly | |
| MockK | 1.14.9 | Kotlin-first mocking library | |
| Mockito + mockito-inline | 5.22.0 / 5.2.0 | Java mocking; coexists with MockK | |
| Hamcrest | 3.0 | Assertion matchers | |
| kotlin-test | 2.0.0-RC1 | kotlin.test assertions (limited use) | |
| Coroutines / Flow | kotlinx-coroutines-test | 1.10.2 | TestDispatcher / runTest |
| Turbine | 1.2.1 | Flow assertion DSL | |
| androidx.arch.core:core-testing | 2.2.0 | InstantTaskExecutorRule for LiveData | |
| UI automation | Espresso | 3.7.0 | XML-View based UI assertions |
| Compose UI Test (junit4) | matches Compose 1.10.4 | Composable semantics tree assertions | |
| androidx.test.ext:junit | 1.3.0 | AndroidJUnitRunner integration | |
| Test Orchestrator | 1.6.1 | Process isolation per test | |
| UiAutomator | 2.3.0 | Cross-app / system-dialog interaction | |
| Hilt-testing | 2.28-alpha | @HiltAndroidTest entry point | |
| Screenshots | Karumi Shot (plugin + runner) | 6.1.0 | Reference-image snapshot regression |
| Coverage | JaCoCo | 0.8.8 | Line + branch coverage; 40.81% threshold |
2. Unit testing — what's in place
Test taxonomy
The ~170 unit-test files break down (approximately) as:
- ~60 in the Costco app module covering legacy logic (Pharmacy, ShoppingList, FindAStore, locale, webview) — the highest concentration of Java tests.
- ~40 across feature/* modules — ViewModel, UseCase, Mapper, Util tests. Examples:
AccountViewModelTest,ProductDetailPageLandingViewModelTest,NativeSearchUseCaseImplTest,ProductPricingCardModelMapperTest. - ~70 across shared/* modules — repository tests, network mappers, formatters. Examples:
BffLayerRepositoryImplTest,ContentstackDeliveryRepositoryImplTest,SharedMembershipServiceTest,StringUtilsKtTest,FirebaseRemoteLoggerTest.
Conventions used
| Aspect | Convention observed | Notes |
|---|---|---|
| File naming | XxxTest.kt / XxxTest.java (e.g. AccountViewModelTest.kt) | Consistent |
| Method naming | camelCase (e.g. fetchUserData_returnsSuccess()) | No backtick/spaced names; Given-When-Then not standardized |
| Mocking | MockK with @MockK + MockKAnnotations.init(this, relaxUnitFun = true) | Mockito coexists in legacy Java tests |
| Assertions | JUnit Assert.assertEquals, occasional Hamcrest matchers | Truth/AssertJ not adopted |
| Coroutines | Custom MainCoroutineRule + UnconfinedTestDispatcher + TestScope | Re-implemented in 7 different modules — see findings below |
| LiveData | @get:Rule InstantTaskExecutorRule() | Standard for ViewModel tests |
| Flow | Turbine flow.test { … } | Used inconsistently — some tests collect manually |
| Test fixtures | Hand-written fakes (e.g. FakeDataStore.kt in shared/auth) | Per-module; no shared :testfixtures module |
Concrete file references
- ViewModel test pattern: Costco/src/test/java/com/costco/app/android/ui/pharmacy/PharmacyViewModelTest.kt
- Mapper test pattern: Costco/src/test/java/com/costco/app/android/warehouse/data/mapper/WarehouseMapperImplTest.kt
- Repository test pattern: feature/productdetaillanding/src/test/java/com/costco/app/productdetaillanding/repo/ProductDetailPageLandingRepositoryImplTest.kt
- Hand-written fake: shared/auth/src/test/java/com/costco/app/auth/util/FakeDataStore.kt
- Custom coroutine rule (7 copies): feature/discover/src/test/java/com/costco/app/shop/MainCoroutineRule.kt · feature/account/src/test/java/com/costco/app/account/MainCoroutineRule.kt · feature/warehouse/src/test/java/com/costco/app/warehouse/MainCoroutineRule.kt · 4 others
3. Automation / UI testing — what's in place
Test types running on device / emulator
| Test type | Framework | Where used |
|---|---|---|
| Compose component test | Compose UI Test (createComposeRule()) | shared/sdui (~40 tests), shared/topbar, shared/navigationheader |
| Espresso XML View test | Espresso onView / onData | Legacy screens in Costco/src/androidTest |
| Page Object Model E2E | Espresso + UiAutomator + Hilt | Costco/src/androidTest/.../costcoUITests/pages/ |
| Hilt instrumentation test | @HiltAndroidTest + HiltAndroidRule | 5+ modules; shared/topbar/.../DefaultNavHeaderTest.kt |
| Screenshot regression | Karumi Shot | feature/account/.../MembershipCardComponentScreenShotTest.kt + others under *ScreenShotTest.kt |
Page Object Model
The androidTest tree under the main app module follows an organized POM-style structure with one file per surface:
- Costco/src/androidTest/kotlin/com/costco/app/android/data/source/local/costcoUITests/pages/HomePageTest.kt
- Costco/src/androidTest/kotlin/com/costco/app/android/data/source/local/costcoUITests/pages/ShopPageTest.kt
- Costco/src/androidTest/kotlin/com/costco/app/android/data/source/local/costcoUITests/pages/SavingsPageTest.kt
- Costco/src/androidTest/kotlin/com/costco/app/android/data/source/local/costcoUITests/pages/SavingsOffersTest.kt
- Costco/src/androidTest/kotlin/com/costco/app/android/data/source/local/costcoUITests/pages/OnBoardingTest.kt
- Centralized test data: .../costcoUITests/TestConstant.kt
UiAutomator falls in for system dialogs (e.g. permission prompts) — see UiDevice.getInstance() usage in HomePageTest.kt.
Compose UI Test pattern
Component-level tests use createComposeRule() + composeTestRule.setContent { … } to mount the Composable in isolation, then assert on semantics nodes:
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
class DefaultNavHeaderTest {
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1) val composeTestRule = createComposeRule()
@Test
fun renders_brand_logo() {
composeTestRule.setContent { CostcoTheme { DefaultNavHeader(...) } }
composeTestRule.onNodeWithTag("brand_logo").assertIsDisplayed()
}
}
Test runner configuration
The app module declares two runners depending on task:
- Default:
androidx.test.runner.AndroidJUnitRunner - For screenshot tests:
com.karumi.shot.ShotTestRunner - Test orchestration:
execution 'ANDROIDX_TEST_ORCHESTRATOR'— each test runs in a separate process to prevent state leakage.
Files: Costco/build.gradle (lines 69–80)
4. Coverage
JaCoCo is wired in via jacoco.gradle at the repo root (toolVersion 0.8.8), with a coverageCheck task that fails the build if line coverage drops below 40.81%.
| Aspect | Detail |
|---|---|
| Coverage tool | JaCoCo 0.8.8 |
| Threshold | 40.81% (line) |
| Reports | HTML + XML, per build variant |
| Excluded from coverage | Activities, Fragments, Compose components, generated DI/Serialization/R/BuildConfig |
| Sonar / SonarCloud | Not detected in CI |
| PR-level coverage diff | Not detected |
5. CI integration
Test execution runs on Azure Pipelines. The pipeline file is azure-pipelines.yml (~50 KB). Highlights:
- PR triggers on
develop,release-*,features/*. - Two-stage flow: Classify (PR metadata) → PR build (gradle build + tests).
- Test results published via
testResultsFiles: '**/TEST-*.xml'. - Gradle parallelism enabled (
--max-workers). - Separate pipeline for security: azure-pipelines-nowsecure.yml runs NowSecure mobile security scans.
- BrowserStack integration is referenced for web testing — no Firebase Test Lab.
6. Findings
Modern Kotlin-first unit-test stack
Page Object Model adopted
costcoUITests/pages/ with TestConstant.kt for data — a maintainable pattern, especially with Hilt and Test Orchestrator.Screenshot regression with Karumi Shot
*ScreenShotTest.kt.Hilt-testing version drift (2.28-alpha vs Hilt 2.56)
hilt_testing is pinned at 2.28-alpha. Mismatched versions risk subtle bugs in test-time DI — generated factories may not align with runtime ones, and bug fixes from 2.28 → 2.56 are missed.hilt-android-testing to match hilt-android (2.56). Run all @HiltAndroidTest suites after the bump.Coverage threshold is too low (40.81%)
MainCoroutineRule duplicated across 7 modules
feature/discover, feature/account, feature/warehouse, and 4 others. Drift is inevitable; one module already uses StandardTestDispatcher while another uses UnconfinedTestDispatcher.shared/testfixtures module exposing MainCoroutineRule, FakeXxx repos, and TestData. Have feature modules testImplementation project(":shared:testfixtures").JUnit 4 + JUnit 5 both available
libs.versions.toml declares both junit 4.13.2 and Jupiter 6.0.3. JUnit 5 lifecycle (@BeforeEach vs @Before) and rule semantics differ; mixing causes confusion.Mockito + MockK both present
.kt files.Heavy reliance on mocks over fakes
FakeAccountRepository, FakeBffService) in the proposed :shared:testfixtures module. Reserve MockK for verifying interactions, not stubbing data.No Macrobenchmark / baseline profile
:macrobenchmark module and no baseline-profile task. Cold-start, scrolling, and frame-drop regressions go unmeasured release-over-release.No Firebase Test Lab in CI
Karumi Shot vs newer alternatives
No Test Retry plugin in Gradle
org.gradle.test-retry with a hard ceiling (1 retry); track flake rate in a CI dashboard.Test naming inconsistency
fun \`returns Loading then Success when fetch succeeds\`()) and document in a CONTRIBUTING.md test section.Mutation testing absent
7. Recommended target state
| Capability | Today | Target (12 months) |
|---|---|---|
| Unit-test coverage | 40.81% threshold | 65–70% line coverage; PR-diff blocking on changed files |
| Coroutine rule | Duplicated across 7 modules | Single source in :shared:testfixtures |
| Mock vs Fake split | Mock-heavy | Fakes for repos/services; MockK for verifying interactions |
| Hilt-testing version | 2.28-alpha | 2.56 matching production |
| Screenshot tests | Karumi Shot | Roborazzi (or Paparazzi) for Compose; off-emulator |
| Performance testing | None | Macrobenchmark + baseline profile shipped in APK |
| Device matrix | Single GMD | Firebase Test Lab nightly across 3–5 devices |
| Mutation testing | None | Pitest on critical modules (productdetaillanding, account) |
| Flake handling | Manual | Gradle test-retry + flake dashboard |
| Test reporting | JUnit XML in Azure | Azure + Codecov/SonarCloud + PR diff annotations |