Testing BookNest End to End

The top of the pyramid starts the real MainActivity with its real navigation, ViewModels, Hilt graph and Compose 234 UI, and replaces only what makes a test flaky: the network. In src/androidTest, AssetDataModule, a @TestInstallIn(replaces = [DataModule::class]) module, binds the asset-backed repository of Hilt and Koin, so the test sees all six books with no server. It then walks a customer's path: open a book, add it, go back, add another, open the cart and check the total:

src/androidTest/.../BookNestEndToEndTest.kt: two books into the cartKotlin
@HiltAndroidTest
...
class BookNestEndToEndTest {
  @get:Rule(order = 0) val hilt = HiltAndroidRule(this)
  @get:Rule(order = 1) val compose = createAndroidComposeRule<MainActivity>()
  @Before fun waitForCatalog() {
    compose.waitUntilAtLeastOneExists(hasText("The Quiet Harbor"), timeoutMillis = 10_000)
  }
  @Test fun addTwoBooksAndCheckTheTotal() {
    compose.onNodeWithText("Salt and Saffron").performClick()
    compose.onNodeWithText("Add to cart").performClick()
    compose.onNodeWithText("In your cart").assertIsDisplayed()
    compose.runOnUiThread { compose.activity.onBackPressedDispatcher.onBackPressed() }  // Back
    compose.onNodeWithText("The Quiet Harbor").performClick()
    compose.onNodeWithText("Add to cart").performClick()
    compose.onNodeWithText("Cart").performClick()
    compose.onNodeWithText("Cart: 2 books, $38.99").assertIsDisplayed()
  }
  ...
}

waitUntilAtLeastOneExists covers the repository's one-second load, which idling alone can't see because it runs on Dispatchers.IO. A second test types "the" into the search field and checks that Patterns of the Deep Web is displayed and Salt and Saffron no longer exists. Both passed in Instrumented Tests's run.

BookNest now has 11 JVM tests (4 under Robolectric, about 20 seconds in all) for every commit and 2 device tests for every release. For flows that leave the app, such as a notification tap, UI Automator 2.4.0 234 drives the whole device, and Maestro (Maestro) runs the same YAML flows against a native Kotlin app.