Android Testing

Testing ViewModel

ViewModel is a part of the Android Jetpack library and is designed to store and manage UI-related data in lifecycle-conscious way. It allows data to survive configuration changes such as screen rotation and it also separates the UI data from the Activity or Fragment, making it easier to test and maintain.
ViewModel can also communicate with a repository to get data from different sources like a database or remote API. In this case ViewModel is not responsible for handling data storage and it just receives the data from repository and it is used to update the UI.

There are several ways to test a ViewModel. If the ViewModel doesn’t get data from the repository, we can test it directly by creating ViewModel instance in the test class and than write the test methods.

If the ViewModel gets data from the repository, we need to take care of the repository class first, and then work with the ViewModel. For this purpose we will create a fake repository and pass it to the ViewModel:

    @Before
    fun Setup() {
        fal fakeRepository = FakeTestRepository()
        val viewModel = MyViewModel(fakeRepository)

FakeRepository extends BaseRepository interface (same as in DefaultRepository that we use in main program) and create temporary list that we will use for testing instead of the Room database.


class FakeTestRepository: BaseRepository {

    private val imageItemList = mutableListOf<ImageDataModel>()

    override var allImagesFromDao: Flow<List<ImageDataModel>> = flow { emit(imageItemList) }

    override suspend fun insert(imageDataModel: ImageDataModel): Long {
        imageItemList.add(imageDataModel)
        return 1    }

    override suspend fun update(imageDataModel: ImageDataModel): Int {
        imageItemList.add(imageDataModel)
        return 1    }

    override suspend fun delete(imageDataModel: ImageDataModel): Int {
        imageItemList.remove(imageDataModel)
        return 1    }

    override suspend fun deleteAll(): Int {
        imageItemList.removeAll(imageItemList)
        return 1    }
}

In the test folder we will create MyViewModelTest class and annotated it with @RunWith(MockitoJUnitRunner::class). After that we need to define InstantTaskExecutorRule which swaps the background executor used by the Architecture Components with a different one which executes each task synchronously.

@RunWith(MockitoJUnitRunner::class)
class MyViewModelTest {

    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()

    @OptIn(ExperimentalCoroutinesApi::class)
    @get:Rule
    var mainCoroutineRule = MainCoroutineRule()

The second rule is MainCoroutineRule() which we use to test coroutines. Since coroutines can be asynchronous and run across multiple threads, we meed to use TestDispachers to run the test on a single test thread if new coroutines are created during the test.

@ExperimentalCoroutinesApi
class MainCoroutineRule (
    private val dispatcher: CoroutineDispatcher = TestCoroutineDispatcher()
    ) : TestWatcher(), TestCoroutineScope by TestCoroutineScope(dispatcher) {

        override fun starting(description: Description?) {
            super.starting(description)
            Dispatchers.setMain(dispatcher)
        }

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

Our ViewModel uses LiveData, and to be able to access that data we need to use Observer and CountDownLatch in our code:

    @Test
    fun `insert data, check if data are inserted and return success`(){

        val  img1 = ImageDataModel(id = 1, "http://www.example.com/img1.png", "image1")
        viewModel.insert(img1)

        val latch = CountDownLatch(1)
        val observer = Observer<List<ImageDataModel>> {
                assertEquals(img1, it[0])
                latch.countDown()
            }
        viewModel.getSavedImages().observeForever(observer)

        if (!latch.await(2, TimeUnit.SECONDS)) {
            throw TimeoutException("LiveData value was never set.")
        }
    }

The CountDownLatch counter is initialized with a number of threads. The counter is decremented each time a thread completes its execution. When the count reaches zero, it means that all threads have completed their execution, and the main thread waiting on the latch resumes the execution. That way we can observe our livedata and get necessary information for test. To avoid duplication code in each test, we can use a generic function that we can call from our test:

    /* Copyright 2019 Google LLC.
   SPDX-License-Identifier: Apache-2.0 */
    fun <T> LiveData<T>.getOrAwaitValue(
        time: Long = 2,
        timeUnit: TimeUnit = TimeUnit.SECONDS
    ): T {
        var data: T? = null
        val latch = CountDownLatch(1)
        val observer = object : Observer<T> {
            override fun onChanged(o: T?) {
                data = o
                latch.countDown()
                this@getOrAwaitValue.removeObserver(this)
            }
        }
        this.observeForever(observer)

        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }
        @Suppress("UNCHECKED_CAST")
        return data as T
    }

And now the test functions can look like this:

    @Test
    fun `insert data, get list from livedata, return success if equal`() {

        val  img1 = ImageDataModel(id = 1, "http://www.example.com/img1.png", "image1")
        viewModel.insert(img1)

        assertEquals(viewModel.getSavedImages().getOrAwaitValue()[0].title, "image1")
        assertEquals(viewModel.getSavedImages().getOrAwaitValue()[0], img1)
    }

And for validating input in our ViewModel we can do:

    @Test
    fun `verify user input, empty value in title input field, return false`() {
        val result = viewModel.validateInput(
            "",
            "http://www.example.com/image2.jpg"
        )
        assertThat(result).isFalse()
    }

Test function for deleting item:

    @Test
    fun `delete item, return true`() {
        val item = ImageDataModel(id = 1, "www.example.com/image2.jpg", "image2")
        viewModel.insert(item)
        viewModel.deleteItem(item)
        assertThat(viewModel.stmessage.value).isEqualTo("1 Row deleted successfully")
    }

The rest of the code you can find here

Testing Room database (with Co-routines and Flow)

Room is a persistence library for Android that provides an abstraction layer over the SQLite database. With the help of the Room, we can easily create the database and perform CRUD operations.
For observing changes to the database and for receiving updates in a real-time Room can utilize Flow, an asynchronous data stream that sequentially emits data.
The basic components of Room are Entity, Dao and Database classes and for this testing we will use the Dao class because it contains all the necessary functions to access data in our database:

@Dao
interface ImageDao {

    @Query("SELECT * FROM image_data_table")
    fun getAllData(): Flow<List<ImageDataModel>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertData(imageDataModel: ImageDataModel): Long

    @Update
    suspend fun updateData(imageDataModel: ImageDataModel): Int

    @Delete
    suspend fun deleteData(imageDataModel: ImageDataModel): Int

    @Query("DELETE FROM image_data_table")
    suspend fun deleteAll(): Int

}

The recommended approach for testing database implementation is writing a JUnit test that runs on an Android device. Because these tests don’t require creating an activity, they should be faster to execute than UI tests.

In the setup() function we have to create new instances of Database for testing purpose. For creating a new instance of database we have to use Room.inMemoryDatabaseBuilder() instead of Room.databaseBuilder() we normally use. inMemoryDatabaseBuilder() creates an in memory version of database. Information stored in an in memory database disappears when the process is killed.

    @Before
    fun setup() {
        database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            ImageDataBase::class.java)
            .allowMainThreadQueries()
            .build()
        dao = database.imageDao()
    } 

For accessing data in the Flow we have to stop the thread. By using a CountDownLatch we can cause a thread to block until other threads have completed a given task. A CountDownLatch is initialized with a given count value. We call the await() method of CountDownLatch to wait till the counter reaches 0, and then we execute our normal code flow. getAllData() method in Dao returns a Flow of a list of objects and the collect method is then used to observe the Flow and receive updates.

    private fun getDataFromFlow() = runBlocking {
        val latch = CountDownLatch(1)
        val imageList = mutableListOf<ImageDataModel>()
        val job = launch(Dispatchers.IO) {
            dao.getAllData().collect { items ->
                imageList.addAll(items)
                latch.countDown()
            }
        }
        withContext(Dispatchers.IO) {
            latch.await(2, TimeUnit.SECONDS)
        }
        job.cancelAndJoin()
        return@runBlocking imageList
    }

And after that we can use return data for testing individual functions in Dao. For example:

    @Test
    fun update_Image_In_Db_should_contain_new_data() = runBlocking {
        val imageItem = ImageDataModel(id = 1, "url1", "image1")
        dao.insertData(imageItem)
        val updatedImageItem = ImageDataModel(id = 1, "url2", "image2")
        dao.updateData(updatedImageItem)

        val imageList = getDataFromFlow()
        assertTrue(imageList.contains(updatedImageItem))
    }

or directly assert data inside .collect block of the code:

    @Test
    @Throws(Exception::class)
    fun insert_Image_In_Db_Return_True() = runTest {
        val imageItem = ImageDataModel(id = 1, "url1", "image1")
        dao.insertData(imageItem)
        val latch = CountDownLatch(1)
        val job = launch(Dispatchers.IO) {
            dao.getAllData().collect { items ->
                assertTrue(items.contains(imageItem))
                latch.countDown()
            }
        }
        latch.await(2, TimeUnit.SECONDS)
        job.cancel()
    }

The complete code you can get here.

Espresso – UI Testing Framework for Android

Testing your app is an integral part of the app development process. By running tests against your app consistently, you can verify your app’s correctness, functional behavior, and usability before you release it publicly.

Espresso is a testing Framework for writing automated UI – user interface tests for your Android application. With espresso, we can simulate the user interaction in our application, but instead a real user is using the app, with espresso we will automate the process by writing code in Java or Kotlin language.
It has been developed by Google and it allows both black-box testing as well as testing of individual components during development cycles.
Automated testing is an integral part of the development lifecycle.

The main components of Espresso are:

  • Espresso – Entry point to interactions with views (via onView() and onData()).
  • ViewMatchers – A collection of objects that implement the Matcher<? super View> interface. You can pass one or more of these to the onView() method to locate a view within the current view hierarchy.
  • ViewActions – A collection of ViewAction objects that can be passed to the ViewInteraction.perform() method, such as click().
  • ViewAssertions – A collection of ViewAssertion objects that can be passed the ViewInteraction.check() method. Most of the time, you will use the matches assertion, which uses a View matcher to assert the state of the currently selected view.
onView(withId(R.id.my_view))        // withId(R.id.my_view) is a ViewMatcher
    .perform(click())               // click() is a ViewAction
    .check(matches(isDisplayed()))  // matches(isDisplayed()) is a ViewAssertion

In Espresso equivalent to findViewById() is onView(withId(R.id.my_view)). If you want to narrow down your search, combination matches can be used: onView(allOf(withId(R.id.my_view), withText("Hello!"))) or onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))
If the target view is inside an AdapterView, such as ListView, GridView, or Spinner, the onView() method might not work. In these cases, you should use onData() instead.

When we have found a suitable matcher for the target view, it is possible to perform instances of ViewAction on it using the perform method.

For example, to click on the view we can use: onView(...).perform(click()) or if we want to execute more than one action with one perform call: onView(...).perform(typeText("Hello"), click())

If the view we are working with is located inside a ScrollView (vertical or horizontal), to ensures that the view is displayed before proceeding to the other action we have to use: scrollTo(): onView(...).perform(scrollTo(), click())

Assertions can be applied to the currently selected view with the check() method. The most used assertion is the matches() assertion. It uses a ViewMatcher object to assert the state of the currently selected view.

For example, to check that a view has the text "Hello!" use: onView(...).check(matches(withText("Hello!")))

If you want to assert that a view with the text "Hello!" is visible – for example after a change of the views visibility flag you can use:
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()))

Example Click on the button test:
onView(withId(R.id.button_simple)).perform(click())

To verify the TextView text you can use:
onView(withId(R.id.text_simple))
.check(matches(withText("Hello Espresso!")))

AdapterView is a special type of widget that loads its data dynamically from an Adapter.
The most common example of an AdapterView is ListView. As opposed to static widgets like LinearLayout, only a subset of the AdapterView children may be loaded into the current view hierarchy. A simple onView() search would not find views that are not currently loaded.

Espresso handles this by providing a separate onData() entry point which is able to first load the adapter item in question, bringing it into focus prior to operating on it or any of its children.

In example adapter test we have to open the Spinner, select a specific item, and verify that the TextView contains the item. As the Spinner class is based onAdapterView, it is recommended to use onData() instead of onView() for matching the item:
Open the item selection:
onView(withId(R.id.spinner_simple)).perform(click())
Select an item:
onData(allOf(`is`(instanceOf(String::class.java)), `is`("Americano"))).perform(click())
Verify that text is correct:
onView(withId(R.id.spinnertext_simple))
.check(matches(withText(containsString( "Americano"))))

Espresso logs all view actions to logcat. 
Espresso warns users about presence of AdapterView widgets. When an onView() operation throws a NoMatchingViewException and AdapterView widgets are present in the view hierarchy, the most common solution is to use onData(). The exception message will include a warning with a list of the adapter views. You may use this information to invoke onData() to load the target view.

Espresso setup in Android Studio

Add the following lines inside dependencies of your app’s build.gradle file:

androidTestImplementation('androidx.test.espresso:espresso-core:3.4.0')
androidTestImplementation('androidx.test:runner:1.4.0')
androidTestImplementation('androidx.test:rules:1.4.0')

In the same build.gradle add:

defaultConfig {
   ...
   testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

Android Studio creates tests by default in src/androidTest/java/com.example.package/

To create a test configuration in Android Studio, complete the following steps:

  1. Open Run > Edit Configurations.
  2. Add a new Android Tests configuration.
  3. Choose a module.
  4. Add a specific instrumentation runner: androidx.test.runner.AndroidJUnitRunner
  5. Run the newly created configuration.

The Espresso Cheat Sheet contains most available instances of MatcherViewAction, and ViewAssertion:

For more detailed information releted to app testing you can visit: https://developer.android.com/training/testing