Skip to content

Testing

Albert Pinto edited this page Jul 13, 2021 · 1 revision

When implementing the application, we need to perform some tests to verify that our code behaviour is working. In Android, there are two types of tests:

  • Unit tests: tests that do not depend on Android classes.
  • Instrumental tests: tests for Android classes, like UI and database.

In the project, we are currently implementing the unit tests. The type of classes that needs to be tested are the following:

  • ViewModels
  • Use cases
  • Repositories
  • Data sources
  • DAOs (except locals, since they are autogenerated)

Testing classes

We are using the JUnit 4 library to implement and run the unit tests. All test classes are organized in the same way to improve readability:

  1. Instance of the class to test
  2. Common variables used in tests
  3. Mocks
  4. Rules (optional)
  5. setUp() method, which initializes the mocks and the instance to test
  6. tearDown() method (optional), which clears the possible effects of a test
  7. Tests
class RecipesViewModelTest {
    private lateinit var viewModel: RecipesViewModel
    
    private val recipes = listOf(
        LocalRecipe(recipeId = 1,
            name = "Chicken Fried",
            type = listOf(RecipeType.Meat),
            description = "It's delicious!",
            time = 20,
            image = ""
        )
    )
    
    @MockK
    private lateinit var getAllRecipes: GetAllRecipes

    @get:Rule
    var mainCoroutineRule = MainCoroutineRule()

    @get:Rule
    var instantExecutionRule = InstantTaskExecutorRule()

    @Before
    fun setUp() {
        getAllRecipes = mockk()
        viewModel = RecipesViewModel(getAllRecipes)
    }
}

For each public method or calculated property of a class, we need to define a set of functions that test each possible scenario.

Naming tests

In Kotlin, test methods are allowed to have spaces in their name using the backticks, which have to follow the when ... then ... pattern.

@Test
fun `when there is an unexpected error then OtherError state is loaded`() {
    // ...
}

Testing suspend methods

To test suspend methods, we need to use the runBlockingTest() method, which will run the test within a dispatcher.

@Test
fun `when there is no error then the recipe is created`() = runBlockingTest {
    // ...
}

Assertions

To conclude whether a test is passed or not, we are using the Hamcrest library. However, we have defined a set of methods that we can use in those asserts:

  • isEqualTo(): since is is a keyword in Kotlin, to use the is() method from Hamcrest, we would have to use backticks; thus, this method provides a more readable way.
  • isTrue(): check if the expression is true
  • isFalse(): check if the expression is false
  • isEmpty(): check if an string is empty
  • isResultSuccess(): check if the result of a use case is a success
  • isResultError(): check if the result of a use case is an error

Rules

To test the ViewModel, we have to use some rules to deal with coroutines.

@get:Rule
var mainCoroutineRule = MainCoroutineRule()

@get:Rule
var instantExecutionRule = InstantTaskExecutorRule()

Mocking dependencies

Whenever a class have a dependency with another, and the instance is passed as a parameter in the constructor (constructor injection), we need mock the class or interface. In the project, we are using MockK, a library build for kotlin to create and use mocks.

Defining a mock

To define a mock, we have to define a lateinit variable and annotate it with @MockK. This variable will be initialized in the setUp() method, which will be executed before each test due to the @Before annotation.

class RecipesViewModelTest {
    private lateinit var viewModel: RecipesViewModel
    
    @MockK
    private lateinit var getAllRecipes: GetAllRecipes
    
    @Before
    fun setUp() {
        getAllRecipes = mockk()
        viewModel = RecipesViewModel(getAllRecipes)
    }

Mocking methods

When using mocks, we have to define how the invoked methods will behave. We have to use the every() or coEvery() methods followed by the returns() or throws() infix methods.

coEvery { 
    getAllRecipes.execute(any()) 
} returns UseCaseResult.Error(CommonException.OtherError(msg))

Verifying methods

When we want to verify that some methods are called, especially when they return Unit, we have to use the verify() or coVerify() methods. We may pass the number of times these functions are called the exactly parameter, being 1 the default value. This is useful when we want to check that a method is not called.

coVerify {
    recipeDao.getIngredient(any())
    recipeDao.insertRecipeIngredient(any())
}

coVerify(exactly = 0) {
    recipeDao.insertIngredient(any())
}

If the order in which the methods are called does not matter, we may use the verifyAll() or coVerifyAll() methods.