Android Shorts 🩳: Testing Toasts with Espresso
On a long enough timeline, every test is a flaky test
Now why would anyone use this?
class ToastMatcher : TypeSafeMatcher<Root?>() {
override fun describeTo(description: Description?) {
description?.appendText("is toast")
}
override fun matchesSafely(item: Root?): Boolean {
val type: Int? = item?.getWindowLayoutParams()?.get()?.type
if (type == WindowManager.LayoutParams.TYPE_TOAST) {
val windowToken: IBinder = item.getDecorView().getWindowToken()
val appToken: IBinder = item.getDecorView().getApplicationWindowToken()
if (windowToken === appToken) { // means this window isn't contained by any other windows.
return true
}
}
return false
}
}ToastMatcher.kt
- Toasts cover the UI for a while, preventing other actions and matchers from working.
- Time sensitive.
- We are in compose world and..
- Matchers suck. 🫠
The only good toast is French Toast
Create a generic Toaster interface that will be responsible for all toasts in the app. Install the implementation into the SingletonComponent :
@Module
@InstallIn(SingletonComponent::class)
object ToastModule {
@Singleton
@Provides
fun providesToaster(
@ApplicationContext context: Context,
): Toaster = object : Toaster {
override fun showToast(text: String) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
}
}
}
interface Toaster {
fun showToast(text: String)
}ToastModule.kt
Add the hilt-testing dependency
dependencies {
// For instrumented tests.
androidTestImplementation("com.google.dagger:hilt-android-testing:2.44")
}
build.gradle.kts
Time to leverage TestInstallIn.
Override
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [ToastModule::class],
)
object OverrideToastModule {
@Singleton
@Provides
fun providesToaster(): Toaster = FakeToaster
}
object FakeToaster : Toaster {
val toasts = mutableListOf<String> ()
override fun showToast(text: String) {
toasts.add(text)
}
}
OverrideToastModule.kt
Any action that should have shown a Toast before, will now add an element in a list instead when running UI tests.
In reality
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject lateinit var toaster: Toaster
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Button(
onClick = { toaster.showToast("hello") },
content = {
Text(text = "show toast")
}
)
}
}
}MainActivity.kt
This will just show a good old Toast when ran normally.
In testing
@HiltAndroidTest
class MainActivityTest {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<MainActivity>()
@Test
fun check_that_my_toast_was_shown() {
composeTestRule.apply {
onNodeWithText("show toast").performClick()
assertNotNull(FakeToaster.toasts.firstOrNull { it == "hello" })
}
}
}MainActivityTest.kt
Anyways
Later.