-
Notifications
You must be signed in to change notification settings - Fork 1
Testing
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)
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:
- Instance of the class to test
- Common variables used in tests
- Mocks
- Rules (optional)
-
setUp()
method, which initializes the mocks and the instance to test -
tearDown()
method (optional), which clears the possible effects of a test - 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.
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`() {
// ...
}
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 {
// ...
}
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()
: sinceis
is a keyword in Kotlin, to use theis()
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
To test the ViewModel,
we have to use some rules to deal with coroutines.
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
@get:Rule
var instantExecutionRule = InstantTaskExecutorRule()
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.
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)
}
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))
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.
Table of contents
Updates