Blog Infos
Author
Published
Topics
, , ,
Published

This is the first of three articles about Jetpack Compose UI Testing that I will be publishing in the next few weeks. Within these articles, I will explain some of the available UI Testing options to apply in production projects. I will begin each article with an introduction into some basic/general aspects, from the most practical perspective.

First of all, let me present a very concise overview without being too repetitive (there is a lot of information on the main Android site, if you wish to check all of the general details).

  • Jetpack Compose uses semantics to give meaning to a piece of UI, apart from that, it’s primarily used for accessibility.
Compose UI Tests using Testing in Isolation 🔬

One of the biggest advantages of using Jetpack Compose is that it allows you to start an activity displaying any composable: your full application, a single screen, or a small element.

  • It allows you to check that composables are correctly encapsulated and they work independently, allowing for easier and more focused UI testing.

You might have defined, in your EntryPointActivity, something like this:

setContent {
SpaceXTheme {
val bottomNavigationItems = listOf(
BottomNavigationScreens.Dashboard,
BottomNavigationScreens.Launches
)
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation(navController, bottomNavigationItems)
}
) {
NavHost(navController, startDestination = BottomNavigationScreens.Dashboard.route) {
composable(BottomNavigationScreens.Dashboard.route) {
InitDashboardScreen()
}
composable(BottomNavigationScreens.Launches.route) {
InitLaunchesScreen()
}
}
}
}

As you can see, there’s a BottomNavigation (with two navigation items) and therefore two tabs: DashboardScreen and LaunchesScreen. But you may not want to test the whole UI structure defined in the Activity. Instead, you can simply test one of the tabs directly (specific screen):

@Test
@InternalCoroutinesApi
fun elementsVisibilityAfterTwoItemsRetrieved() {
composeTestRule.apply {
setContent {
SpaceXTheme {
LaunchesScreen(
state = LaunchesContract.State(
listOf(
LaunchUiModel(
"Mission1", "08-12-2021", true, "0", RocketUiModel(
"Rocket1", "Rocket Type1"
), LinksUiModel("", "", "Youtube Link")
),
LaunchUiModel(
"Mission2", "09-12-2021", false, "0", RocketUiModel(
"Rocket2", "Rocket Type2"
), LinksUiModel("", "WikiPedia Link", "Youtube Link")
)
), isLoading = false, isError = false
),
effectFlow = flow { emit(LaunchesContract.Effect.ClickableLink.None) } )
}
}
onNodeWithText("Mission1", useUnmergedTree = true)
.assertIsDisplayed()
onNodeWithText("Mission2", useUnmergedTree = true)
.assertIsDisplayed()
onAllNodesWithContentDescription(
"Item",
substring = true,
useUnmergedTree = true
).assertCountEquals(numItemsShowed)
}
}

The test is focused on one screen,
rather than in the whole Activity content

 

LaunchesScreen is the one set in the content (by setContent) and the only UI State specified is the one for that screen too.

Progressively it’s possible to specify smaller parts of the UI and test all of them in isolation to check any specific case.

Compose UI Tests using MockWebServer ⚡️

MockWebServer is a library from Square that allows you to specify which responses to return and then verify that requests were made as expected.

Advantages
  • Tests using MockWebServer can coexist with the ones using Testing in Isolation.
Configuration
  1. Add the next dependency into the build.gradle (app level)
testImplementation("com.squareup.okhttp3:mockwebserver:4.9.3")

2. Define a MockTestRunner and link it through your build.gradle (app)

android {
defaultConfig {
applicationId = "prieto.fernando.sample"
testInstrumentationRunner = "prieto.fernando.sample.webmock.MockTestRunner"
}
buildFeatures {
compose = true
viewBinding = true
}
// rest of configuration
}

MockTestRunner needs to use a TestApplication class, which is conveniently provided by Dagger Hilt:

class MockTestRunner : AndroidJUnitRunner() {
override fun onCreate(arguments: Bundle?) {
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().permitAll().build())
super.onCreate(arguments)
}
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}

Job Offers

Job Offers


    Android App Developer

    sipgate GmbH
    Düsseldorf, Remote
    • Full Time
    apply now

    Android Build Engineer

    Pinterest
    San Francisco, CA | Seattle, WA
    • Full Time
    apply now

    Senior Mobile Systems SDK Engineer

    Sauce Labs
    Remote
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

Jobs

3. Create a FakeNetworkModule to replace the original one (NetworkModule) in the tests configuration.

Let’s define our baseUrl in a way that can be overridden in the fake module:

@InstallIn(SingletonComponent::class)
@Module
open class NetworkModule {
open fun getBaseUrl () ="https://api.spacexdata.com/v3/"
@Provides
@BaseUrl
fun provideBaseUrl() = getBaseUrl ()
// The rest of Builders, Factories, etc. are omitted for simplicity
}

Hilt provides a very intuitive and simple solution to replace modules, for testing purposes. By simply pointing to the target module that’s being overridden and selecting the local host that MockWebServer needs to point to, it’s possible to get this network setup done. The Port can be 8080 defined in the build.gradle file.

@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [NetworkModule::class]
)
class FakeNetworkModule : NetworkModule() {
override fun getBaseUrl() = "http://127.0.0.1:${BuildConfig.PORT}"
}

4. Define your own Dispatchers.

By default MockWebServer uses a queue to specify a series of responses. Use a Dispatcher to handle requests using another policy. One natural policy is to dispatch on the request path. You can, for example, filter the request instead of using server.enqueue().

A common practice is to define a SuccessDispatcher and an ErrorDispatcher. Those two classes will deliver the specified responses in each case (success & error) and will be obtained from the path indicated.

Once the path to the expected responses is specified (JSON files), MockWebServer will be able to parse the content and offer it automatically in your tests.

 Responses files defined within the project(debug directory, same level as androidTest)

5. Initialise MockWebServer in your tests.

val mockWebServer by lazy { MockWebServer() }
@Before
fun setUp() {
mockWebServer.start(BuildConfig.PORT)
}
@After
fun teardown() {
mockWebServer.shutdown()
}
view raw BaseTest.kt hosted with ❤ by GitHub

6. Start testing using MockWebServer.

@ExperimentalMaterialApi
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
class LaunchesScreenKtTest : BaseScreenTest() {
@Test
@InternalCoroutinesApi
fun visibleItemsCountAfterOpeningTheScreen() {
mockWebServer.dispatcher = SuccessDispatcher()
setMainContent()
composeTestRule.apply{
onNodeWithText("Launches").assertIsDisplayed().performClick()
mainClock.advanceTimeBy(2000)
onAllNodesWithContentDescription(
"Item",
substring = true,
useUnmergedTree = true
).assertCountEquals(6)
}
}
}

If you need to check more details and implement the whole configuration I have put in place, make sure you check my Github example SpaceX prepare for Clean Architecture liftoff🚀.

References

Jake Wharton Blog

Arunkumar Blog

Jetpack Compose Official

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
In this part of the series, we will plan our first screen in Jetpack…
READ MORE
blog
Ask yourself a fairly standard question for any interview to fill an Android position:…
READ MORE
blog
This is part of a multi-part series about learning to use Jetpack Compose through…
READ MORE
blog
UI created with XML is traditionally tested with Espresso and UIAutomator. However, Jetpack Compose…
READ MORE

Leave a Reply

Your email address will not be published.

Fill out this field
Fill out this field
Please enter a valid email address.

Menu