Android ViewModel, Repository, Room and Retrofit with Jetpack the testing part using mockk

Alexandre Genet
4 min readJun 2, 2023

--

This article is a follow-up to this story about mixing data on a single compose screen with flow combine. I recommend reading it before this article, as we will discuss how to write unit tests based on this previous example.

Architecture description

The Mars Rover Explorer Android application will be used as an example. This story will focus on the third screen that displays the list of photos from a given rover and date with a status of “saved” or “not saved”. If you want to learn more about the application, a course is available on Udemy for free.

Third screen of Rover Photo Explorer
Third screen of Rover Photo Explorer

This architecture is divided into four layers. Each layer instantiates the one on the right. Data is propagated from right to left by a collected flow.

Android Jetpack Compose ViewModel Repository and Data Layers Schema
Architecture schema with Retrofit and Room

This article will focus on the repository layer mixing data from the Retrofit interface and Room interface into a UI model used by the ViewModel and Activities.

Test setup

First, we want to create a MainDispatcherRule to use a UnconfinedTestDispatcher in our test.

The UnconfinedTestDispatcher is a TestDispatcher that skips delays and executes tasks not confined to any particular thread. For more details, follow the documentation about UnconfinedTestDispatcher.

class MainDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}

override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
}

Writing the test

Then apply this MainDispatcherRule to the test with @get:Rule.

@get:Rule
val coroutineRule = MainDispatcherRule()

The next step is to define the Service and the Dao using mockk.

Mockk is a similar library to Mockito. It allows the developer to control the return of methods of classes passed in parameters.

private val marsRoverPhotoService = mockkClass(MarsRoverPhotoService::class)
private val marsRoverSavedPhotoDao = mockkClass(MarsRoverSavedPhotoDao::class)

Then create the test with the annotation @Test and runTest with the parameter coroutineRule.testDispatcher.

@Test
fun `should emit success when service and dao return valid data`() =
runTest(coroutineRule.testDispatcher) {

The test will be divided into three parts: Given, When and Then.

For the Given part, we will create a fake RoverPhotoRemoteModel. Then use coEvery to return this RoverPhotoRemoteModel when getMarsRoverPhotos() is called.

We will also use coEvery over allSavedIds() and make return a flow of a list of Int. Returning the id 2 will define that the rover with the id 2 is saved.

//Given
val roverPhotoRemoteModel = RoverPhotoRemoteModel(
photos = listOf(
PhotoRemoteModel(
camera = CameraRemoteModel(
fullName = "Camera One",
id = 1,
name = "Camera 1",
roverId = 1
),
earthDate = "2022-07-02",
id = 2,
imgSrc = "https://example.com/photo1",
rover = RoverRemoteModel(
id = 5,
landingDate = "2021-02-18",
launchDate = "2020-07-30",
name = "Perseverance",
status = "active"
),
sol = 20
),
PhotoRemoteModel(
camera = CameraRemoteModel(
fullName = "Camera Two",
id = 3,
name = "Camera 2",
roverId = 1
),
earthDate = "2022-07-02",
id = 4,
imgSrc = "https://example.com/photo2",
rover = RoverRemoteModel(
id = 5,
landingDate = "2021-02-18",
launchDate = "2020-07-30",
name = "Perseverance",
status = "active"
),
sol = 20
)
)
)
coEvery {
marsRoverPhotoService.getMarsRoverPhotos("perseverance", "0")
} returns roverPhotoRemoteModel
coEvery {
marsRoverSavedPhotoDao.allSavedIds("0", "perseverance")
} returns flowOf(listOf(2))

For the When part of the test, instantiate the MarsRoverPhotoRepo with the mocked service and dao.

Then call the getMarsRoverPhoto() use toList() to convert the flow to a list.

//When
val marsRoverPhotoRepo = MarsRoverPhotoRepo(marsRoverPhotoService, marsRoverSavedPhotoDao)
val result = marsRoverPhotoRepo.getMarsRoverPhoto("perseverance", "0").toList()

For the Then part, create a val expectedResult make sure the rover with ID 2 is saved.

Then compare the result size and the first item of the result, it should be equals to expectedResult.

//Then
val expectedResult = RoverPhotoUiState.Success(
roverPhotoUiModelList = listOf(
RoverPhotoUiModel(
id = 2,
roverName = "Perseverance",
imgSrc = "https://example.com/photo1",
sol = "20",
earthDate = "2022-07-02",
cameraFullName = "Camera One",
isSaved = true
),
RoverPhotoUiModel(
id = 4,
roverName = "Perseverance",
imgSrc = "https://example.com/photo2",
sol = "20",
earthDate = "2022-07-02",
cameraFullName = "Camera Two",
isSaved = false
)
)
)
assertEquals(1, result.size)
assertEquals(expectedResult, result[0])

This wraps up this example. We have mocked two different data sources and verified the result.

If you want to learn how to create step by step, the Mars Rover Photo Explorer Android App an Udemy online tutorial is available for free. It covers Jetpack Compose, ViewModel, Repositories, Retrofit, Room and Hilt. It contains more than 7h of live coding where you learn to create Jetpack Compose screens, Compose navigation, StateFlow and Flow combine. You will customize the theme with Material3 and write Unit tests.

The code for the Mars Rover Explorer is on Github.

--

--