DEV Community

Cover image for Best practices for Unit Testing Android Apps with Mockk, Kotest and others
Roman Chugunov
Roman Chugunov

Posted on

Best practices for Unit Testing Android Apps with Mockk, Kotest and others

Perhaps you write unit tests, or maybe you don’t. With this article, I’m not aiming to persuade you to do it, rather I’d like to demonstrate how far the world of unit tests has advanced and how much easier it is to write unit tests now, as opposed to how Android developers did it 10 years ago. Please also note that I’ll be talking about testing done by developers and not by QA engineers, so we won’t go into high-level frameworks like Appium.

From the point of view of the Test Pyramid, in this article, I will only describe the lower layer, namely unit tests. However, I’m planning on writing one or two other articles dedicated to UI tests with frameworks like Espresso, as well as code coverage and visual reporting for test results. Right now, I’m not planning on touching on the subject of CI/CD setup as it’s quite a major topic, and mobile developers are often not responsible for it (or not all developers).

Image description

So, unit tests are tests that cover the logic of individual parts of an app, be it ViewModel, Repository, Usecase, DataSource, or other components. Normally unit tests do not test Activity and other Android components, because, in such a case, we would have to run the tests on an Android device or an emulator, while Repository testing can be done on a local computer where your development environment is installed. Of course, there is the Robolectric framework, but it rather deals with testing UI components.

Ten years ago, classical unit tests for Android were written using the JUnit framework, and even now it’s the most popular framework for such tests. Let’s look at some of its features.

JUnit

Currently, we have two versions of this framework, JUnit 4 and JUnit 5. The first letter in the name means that the framework was originally developed to test Java code, but it works perfectly well for Kotlin code, too. Below, you can see several examples of tests written using JUnit 4.

Assume we have a simple ViewModel that includes loadUsers method:

class SimpleViewModel : ViewModel() {

    fun loadUsers(): List<User> {
        return listOf(User(id = "1", userName = "jamie123"))
    }
}
Enter fullscreen mode Exit fullscreen mode

This method is very dumb, so all we can do is to check if it returns what we expect it to return. To do this, we can write the following test.

class SimpleViewModelTest {

    private val viewModel = SimpleViewModel()

    @Test
    fun testReturnUsers() {
        val result = viewModel.loadUsers()
        assertEquals(listOf(User(id = "1", userName = "jamie123")), result)
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we just check if the result of the method execution equals a certain expected value. Assert.asserEquals method helps us with this. The Assert class includes lots of other useful methods for such checks. I recommend going through all the available methods.

Let’s add some logic to our loadUsers method. We add a parameter that will return only adult users (older than 18).

fun loadUsers(onlyAdults: Boolean): List<User> {
    val allUsers = listOf(
        User(id = "1", userName = "jamie123", age = 10),
        User(id = "2", userName = "christy_a1", age = 34)
    )
    return if (onlyAdults) {
        allUsers.filter { it.age >= 18 }
    } else {
        allUsers
    }
}
Enter fullscreen mode Exit fullscreen mode

For this method, we can write two tests: the first one will check the upper branch of if (onlyAdults), and the second—the lower branch.

@Test
fun testReturnOnlyAdultsUsers() {
    val result = viewModel.loadUsers(onlyAdults = true)
    assertEquals(
        listOf(
            User(id = "2", userName = "christy_a1", age = 34)
        ), result
    )
}

@Test
fun testReturnAllUsers() {
    val result = viewModel.loadUsers(onlyAdults = false)
    assertEquals(
        listOf(
            User(id = "1", userName = "jamie123", age = 10),
            User(id = "2", userName = "christy_a1", age = 34)
        ), result
    )
}
Enter fullscreen mode Exit fullscreen mode

So, everything is quite simple. Unit tests allow us to test various scenarios of the logic within methods. We enter some parameters and check the result with the expected value.

JUnit 4 includes basic testing features. Writing tests with it is simple and convenient.

JUnit 5

JUnit 5 has many pleasant additions. Also, the list of key annotations somewhat changed: for example, @Before became @BeforeEach, and @After@AfterEach. In addition, the package hierarchy totally changed. All main classes and annotations are now available through path org.junit.jupiter.api.* . Let’s consider several key features new to JUnit 5.

@DisplayName Annotation

This annotation makes names of your tests more expressive.

@DisplayName("Test return only adults users")
@Test
fun testReturnOnlyAdultsUsers() {
    val result = viewModel.loadUsers(onlyAdults = true)
    assertEquals(
        listOf(
            User(id = "2", userName = "christy_a1", age = 34)
        ), result
    )
}
Enter fullscreen mode Exit fullscreen mode

Although Kotlin developers can use such test naming even without a special annotation, using backticks.

@Test
fun `Test return only adults users`() {
        ...
}
Enter fullscreen mode Exit fullscreen mode

@DisplayName’s advantage is that it can also be applied to the class name.

@DisplayName("Tests for SimpleViewModel")
class SimpleViewModelTest {
Enter fullscreen mode Exit fullscreen mode

Together with such naming of test methods, we can follow the GivenWhenThen approach. So our test will look as follows:

@DisplayName("WHEN pass onlyAdults = true THEN return expected items")
@Test
fun testReturnOnlyAdultsUsers() {
    val result = viewModel.loadUsers(onlyAdults = true)
Enter fullscreen mode Exit fullscreen mode

Or like this:

@Test
fun `WHEN pass onlyAdults = true THEN return expected items`() {
        ...
}
Enter fullscreen mode Exit fullscreen mode

One must admit that now the test name describes its structure more clearly.

@Disabled Annotation

In addition, if you follow TDD and write a lot of tests that don’t always run on the go, JUnit 5 has a convenient annotation @Disabled that allows you to turn off tests that don’t work for some reason or were written for code to be added yet.

@Disabled("Code not implemented yet")
@Test
fun `WHEN pass onlyAdults = true THEN return expected items`() {
Enter fullscreen mode Exit fullscreen mode

Remember that turning off failed or unstable tests is a bad practice. You should repair such tests or the code that made them fail at once.

Parameterized tests @ParameterizedTest

Let’s now consider a case when our method loadUsers receives enum as input.

fun loadUsers(filter: FilterType): List<User> {
    val allUsers = listOf(
        User(id = "1", userName = "jamie123", age = 10),
        User(id = "2", userName = "christy_a1", age = 34)
    )
    return when (filter) {
        FilterType.ADULT_USERS -> allUsers.filter { it.age > 18 }
        FilterType.CHILD -> allUsers.filter { it.age < 18 }
        FilterType.ALL_USERS -> allUsers
    }
}
Enter fullscreen mode Exit fullscreen mode

To check all three conditions, we can write three different tests. Alternatively, we can write a parameterized test that will receive as input a parameters array and expected values and then compare them:

@ParameterizedTest
@MethodSource("testArgs")
fun `WHEN pass onlyAdults = true THEN return expected items`(argAndResult: Pair<FilterType, List<User>>) {
    val result = viewModel.loadUsers(argAndResult.first)
    assertEquals(argAndResult.second, result)
}

companion object {
    @JvmStatic
    fun testArgs(): List<Pair<FilterType, List<User>>> = listOf(
        FilterType.CHILD_USERS to listOf(User(id = "1", userName = "jamie123", age = 10)),
        FilterType.ADULT_USERS to listOf(User(id = "2", userName = "christy_a1", age = 34)),
        FilterType.ALL_USERS to listOf(
            User(id = "1", userName = "jamie123", age = 10),
            User(id = "2", userName = "christy_a1", age = 34)
        )
    )
}
Enter fullscreen mode Exit fullscreen mode

In this example, I use an additional annotation @MethodSource which will include the method providing our parameters. This method must be static (located in a companion object and having @JvmStatic annotation) and return a list or a sequence of parameters. And in the very test method we receive an element of this list. In our case, it’s Pair<FilterType, List<User>>. This way the test method becomes as simple as possible: we just call a tested method with the first test argument value and compare the result with the second one.

Besides MethodSource, there are numerous other sources of arguments for parameterized tests.

Probably, you’ve noticed that we have been testing clean methods, with the result depending neither on the condition of the class nor on other classes. The user list was hardcoded into the method. In real life things are always more complicated. So, let’s take a look at a situation when our view model receives the user list from a repository.

class SimpleViewModel(private val usersRepository: UsersRepository) : ViewModel() {

    fun loadUsers(filter: FilterType): List<User> {

                val allUsers = usersRepository.getUsers()
                return when (filter) {
            FilterType.ADULT_USERS -> allUsers.filter { it.age > 18 }
            FilterType.CHILD_USERS -> allUsers.filter { it.age < 18 }
            FilterType.ALL_USERS -> allUsers
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The purpose is the same: we want to test the logic of the SimpleViewModel method. But we don’t know (and don’t want to know) what UsersRepository returns. For this, we can use a mock framework, for example, Mockito.

Mockito

Mockito and similar frameworks allow us to change the logic of classes and methods which we do not test directly but which are part of the code. In our example, we can simulate the execution of usersRepository.getUsers() method when the list of expected Users is returned to us, in order to check the execution of loadUsers method. I won’t go into much detail about Mockito’s syntax as it has many features and nuances. However, the two key features are: it can replace a method’s returning value; it can also check if the replaced method has been called with the expected parameters. In this case, our test will look like this:

private val mockUsersRepository: UsersRepository = mock()
private val viewModel = SimpleViewModel(mockUsersRepository)

@BeforeEach
fun setup() {

    mockUsersRepository.stub {
        on { getUsers() } doReturn listOf(
            User(id = "1", userName = "jamie123", age = 10),
            User(id = "2", userName = "christy_a1", age = 34)
        )
    }
}

@ParameterizedTest
@MethodSource("testArgs")
fun `WHEN pass onlyAdults = true THEN return expected items`(argAndResult: Pair<FilterType, List<User>>) {
    val result = viewModel.loadUsers(argAndResult.first)
    assertEquals(argAndResult.second, result)
    verify(mockUsersRepository).getUsers()
}
Enter fullscreen mode Exit fullscreen mode

As you can see, I create mockUsersRepository object using mock( function. Then, in setup() method, I set up this repository in such a way so that it would return a list of hardcoded values. After this, on top of everything else, I check if, for mockUsersRepository object, getUsers method was called with verify function, in the test method. It’s important to note that Mockito is a very old framework and was originally written for Java. When Kotlin appeared, it turned out that out of the box this framework can work with some limitations, which is why an extension, mockito-kotlin, was released. The example above was written with it. Another Mockito’s limitation was that it was impossible to mock final classes and static methods, and all classes are final by default in Kotlin. Currently, this is solved through mockito-inline but, before, it was a serious limitation. In respect to Java development, there was and is an alternative for Android developers, PowerMock.

PowerMock

This framework was intended to eliminate Mockito’s limitations with regard to mocking final, static and private methods. It successfully solves this issue but, at the same time, is a bit harder to use. For example, we can mock a static method as follows:

mockStatic(MyClass::class.java)
when(MyClass.firstMethod(anyString())).thenReturn("Some string");
Enter fullscreen mode Exit fullscreen mode

Much of this is now available in Mockito. Even more - naturally Kotlin doesn’t have static methods, and it’s a good practice not to create them. Ideal code is code that follows the SOLID and Clean Architecture principles, which means it can be easily mocked and tested. Therefore, we, Android developers, don’t need PowerMock’s special features but it’s important to remember that such frameworks exist. Another Mockito’s limitation concerns work with coroutines and flows. In such cases, once again, mockito-kotlin as well as third-party libraries like turbine will help you out. This being said, I’d like to tell you about another alternative to Mockito, which is Mockk.

Mockk

Mockk is a rather new framework; it was originally designed for Kotlin, although it supports Java as well. Before mockito-kotlin was released, using Mockk was much easier and beneficial for Kotlin developers, but right now both these frameworks have nearly the same set of features and similar syntax. For example, assume that our function loadUsers is no longer synchronous, but rather suspended:

interface UsersRepository {
    suspend fun getUsers(): List<User>
Enter fullscreen mode Exit fullscreen mode

Regular Mockito can’t mock it. That’s why need the mockito-kotlin extension. With it, our mock will look like this:

@BeforeEach
fun setup() {
    mockUsersRepository.stub { onBlocking { getUsers() } doReturn listOf(User("user_id")) }
}
Enter fullscreen mode Exit fullscreen mode

In Mockk, the mock of the same function will be this way:

@BeforeEach
fun setup() {
    coEvery { getUsers() } returns listOf(User("user_id"))
}
Enter fullscreen mode Exit fullscreen mode

As you can see, it’s a matter of taste.

However, an important distinction between Mockk and Mockito is that all Mockk’s mocks do not have default values. In Mockito, a method that returns Unit will be expectedly called, and a method that returns a reference type will return null. This may cause unfavorable consequences: if we can handle this null value in our production code, it is obvious that the test will work incorrectly hiding the NPE. In Mockk, any method of the mock class throws an exception by default, although it is possible to create relaxed mocks in order to achieve a behavior similar to that of Mockito.

private val mockUsersRepository = mockk<UsersRepository>(relaxed = true)
Enter fullscreen mode Exit fullscreen mode

In fact, it is an anti-pattern because it can lead to developers missing errors hidden by relaxed mocks. However, it may sometimes be useful not to create mocks for functions that return Unit. For this, Mockk has a special parameter:

private val mockUsersRepository = mockk<UsersRepository>(relaxUnitFun = true)
Enter fullscreen mode Exit fullscreen mode

It is probably the main difference between Mockk and Mockito. But it is up to you to decide whether it’s an advantage and whether you should change your framework because of it.

Now that we’ve learned about the frameworks to create mocks for unit tests, I’d like to make the task a bit more complicated. In our example, SimpleViewModel class has only one dependencyusersRepository, and this repository, in turn, contains only one method. In real life, your tested class can have a dozen of dependencies, with a set of methods closely related to each other. That’s why viewModel.loadUsers method may be a lot more complicated. In the example below, we combine several Observables to derive the final result.

fun init() {
        observeUserAvailability()
                .subscribeNext { result ->
                        // check result
                }
}

private fun observeUserAvailability(draftPotUuid: String): Observable<UserAvailability> = Observable.combineLatest(
        usersRepository.observeCurrentUser(),
        profileRepository.observeCurrentProfile(),
        kycRepository.observeKycStatus(),
        addressRepository.loadCurrentUserAddress(),
        countryRepository.observeCountries(),
    )
Enter fullscreen mode Exit fullscreen mode

To test a set of scenarios for init() method, you need to have a set of tests with duplicated methods for mocking observeCurrentUser, observeCurrentProfile , and others. We could add them into BeforeEachmethod but then, we would want to be careful and not to forget to update default mocks where they are not needed. Nested classes of JUnit 5 can help you solve this issue.

Nested classes in JUnit 5

Using nested (or inner for Kotlin) classes, we can group tests with some common conditions. For example, we can create two groups of tests depending on what usersRepository.observeCurrentUser() method returns. In the first case, it will return the correct user User("user_id"), while in the second case—a user with incorrect ID User(null):

@DisplayName("Tests for SimpleViewModel")
class SimpleViewModelTest {

    private val mockUsersRepository: UsersRepository = mock()
    private val viewModel = SimpleViewModel(mockUsersRepository)

    @DisplayName("GIVEN userRepository returns correct user")
    @Nested
    inner class MockGroup1 {

        @BeforeEach
        fun setup() {
            mockUsersRepository.stub { on { getCurrentUser() } doReturn User("user_id") }
        }

        @Test
        fun `GIVEN kycRepository returns kycPassed WHEN init viewModel THEN get expected result`() {
            ///
        }
    }

    @DisplayName("GIVEN userRepository returns incorrect user")
    @Nested
    inner class MockGroup2 {

        @BeforeEach
        fun setup() {
            mockUsersRepository.stub { on { getCurrentUser() } doReturn User(null) }
        }

        @Test
        fun `GIVEN kycRepository returns kycPassed WHEN init viewModel THEN get expected result`() {
            ///
        }
    }
Enter fullscreen mode Exit fullscreen mode

Following this example, we can extend this approach to grouping tests and create as many nested levels as we need, as long as they remain readable and understandable to us and other members of our team. Try to adhere to the KISS principle.

JUnit 5 and the abovelisted frameworks have many other interesting features. I have described only those that I use myself and which I consider highly useful to us, Android developers.

TDD vs BDD

As a matter of fact, in the previous examples, we have shifted a bit away from the TDD standards in the meaning that we test not only the operability of our code, but rather check if the code runs according to certain specifications (Given/When/Then). These specifications are our tests, and the syntactic sugar in the form of the possibility to give clear names to the tests using DisplayName and the grouping of the tests by a set of similar attributes helps us clearly formulate these specifications. There is an entire family of frameworks in different languages that allow us to create such specifications: for Java it is Spock, for Ruby—RSpec, and for Kotlin—Spek and Kotest frameworks. Below, I will go into more detail about them.

Spek

Spek is a BDD framework where we describe the conditions in which the tested object must work and its reaction to these conditions, in detail. We by default make the test body from several units, to differentiate between the areas of responsibility (what we are testing, the conditions and the expected result). If we go back to our SimpleViewModel the tests will look like this:

class SimpleViewModelSpec : Spek({

    val mockUsersRepository: UsersRepository = mock()
    val viewModel by memoized { SimpleViewModel(mockUsersRepository) }

    describe("loadUsers") {

        beforeEachTest {
            // do mocking
        }

        it("should return only adult users if pass filterType: adult users") {
                val result = viewModel.loadUsers(FilterType.ADULT_USERS)
            assertEquals(listOf(User(id = "2", userName = "christy_a1", age = 34)), result)
            verify(mockUsersRepository).getUsers()
        }
    }
})
Enter fullscreen mode Exit fullscreen mode

As you can see, our testing logic is written as a lambda expression, and each test is a kind of mapping from the line, the test name, and the very unit running the test.

The describe-it style is mainly used to write Spek tests. describe structure allows us to create a group of tests describing a specific method, and, in it, a specific scenario for the method is written. But it is also possible to use the GivenWhenThen style. Unfortunately, the framework doesn’t have embedded coroutine support, so we have to always use runBlockingTest { } for suspended functions. There is a ticket with this feature request on Github, but it seems it was never completed.

Spek doesn’t provide its own asserts, mocks or matchers, but it allows you to work with other libraries, such as Mockk and Mockito.

Kotest

Another framework that, in terms of its syntax and behavior, is similar to Spek. It has also recently gained popularity among Android developers thanks to its Kotlin-first principle and flexibility. Perhaps, flexibility first and foremost means that you can choose the style to write your tests. Currently, you can choose from 10 styles. Personally, I prefer FreeSpec. The above tests will look like this if written in Kotest:

class SimpleViewModelSpec : FreeSpec({

    val mockUsersRepository: UsersRepository = mock()
    val viewModel = SimpleViewModel(mockUsersRepository)

    beforeTest {
        // do mocking
    }

    "WHEN pass filterType: adult users" - {

        val result = viewModel.loadUsers(FilterType.ADULT_USERS)

        "THEN return only adult users" {
            assertEquals(listOf(User(id = "2", userName = "christy_a1", age = 34)), result)
            verify(mockUsersRepository).getUsers()
        }
    }
})
Enter fullscreen mode Exit fullscreen mode

The test above works in the FreeSpec style according to the GivenWhenThen principle.

Kotest has embedded coroutine support, which is why, as opposed to Spek, you won’t have to use runBlockingTest { } for suspended functions.

Test isolation level, which differs from JUnit 4/5, is important here. The values of mocks and the test conditions are not cleared after each test. If you want a behavior similar to JUnit, use isolationMode = IsolationMode.InstancePerLeaf flag.

class SimpleViewModelKoTest : FreeSpec({

        isolationMode = IsolationMode.InstancePerLeaf
Enter fullscreen mode Exit fullscreen mode

Also, Kotest provides an additional set of asserts and matchers that may be more convenient than those that we’ve used with Mockito. If your project already uses Mockito and JUnit, that’s alright, as tests written in Kotest can co-exist with JUnit.

Conclusion

Today, there are many unit testing frameworks and libraries for Android apps. I have described only the most popular ones, but there are more. If you work on enterprise Android projects you are most likely used to writing unit tests with some of mentioned in this article technologies. As a rule, large companies use time-tested tools, such as JUnit and Mockito. But, as new Kotlin features continue to be released, numerous new testing libraries and frameworks are appearing. I can definitely recommend you to try out Mockk or Kotest, if you haven’t used them before. They can make your life much easier and improve the quality of your tests.

In the next article, I will tell you about the approaches and best practices used when writing UI tests. Also, please share your own experience in using unit tests in your projects: what frameworks do you use?

Top comments (1)

Collapse
 
mdkabir profile image
Kabir

good