Unit Testing Tutorial for Android: Getting Started
In this Unit Testing Tutorial for Android, you’ll learn how to build an app with Unit Tests in Kotlin. By Lance Gleason.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Unit Testing Tutorial for Android: Getting Started
25 mins
Using Fakes and Mocks
When testing your viewmodel you may have noticed that instead of passing in real implementations of CocktailsRepository
and CocktailsGameFactory
, you passed in custom implementations that created the side effect you wanted it to have for your test. This kind of implementation is called a fake or a stub. It has the advantage of being easier to maintain as your test suite gets larger at the cost of more upfront effort to create the fake. It also ensures you're testing the interface of your class or method, instead of the internals of how it works.
You may also run into a concept called a mock. Using libraries, such as Mockito or MockK, you can create a mock that has the same interface as the object you're replacing. You can configure this mock to return preprogrammed responses. A mock can also register calls made against its methods and have assertions made to see if these methods are called. This has the advantage of being quicker to write your initial tests, at the cost of being more difficult to maintain as your test suite gets larger. Mocks also lead to an anti-pattern where you often end up testing how you're implementing your code, instead of the interface which may lead to fragile tests.
Writing More Tests
Currently, you're able to retrieve a question but don't have a way to get another question. You'll fix that.
First, open CocktailsViewModelTests
and add in the following test:
@OptIn(ExperimentalCoroutinesApi::class) @Test fun nextQuestion_shouldShowNextQuestion() = runTest { val cocktailsViewModel = buildSuccessfulGameViewModel(buildGame(), testScheduler) cocktailsViewModel.initGame() advanceUntilIdle() cocktailsViewModel.nextQuestion() advanceUntilIdle() val question = cocktailsViewModel.question.value Assert.assertEquals(questions.last(), question) }
This test calls nextQuestion()
in your viewmodel to get the next question in the game. Next, run your tests and this will fail.
You already implemented functionality in Game
to get the next question, so to fix this, replace nextQuestion()
in CocktailsViewModel
with the following:
fun nextQuestion() { getGameObject()?.let { rawGame -> _question.update { rawGame.nextQuestion() } } }
Finally, run your tests again and it'll pass.
You currently can't answer questions and keep a score, so you'll fix that. Your Score
object needs some work. Create a new file called ScoreUnitTests.kt in your test directory and paste in the following:
package com.kodeco.cocktails import com.kodeco.cocktails.game.model.Score import org.junit.Assert import org.junit.Test class ScoreUnitTests { @Test fun whenIncrementingScore_shouldIncrementCurrentScore() { val score = Score() score.increment() Assert.assertEquals("Current score should have been 1", 1, score.current) } }
Next, run this test and it'll fail.
That's because the score isn't being incremented when you call increment()
. To fix that, open Score
in game ▸ model and replace increment()
with the following:
fun increment() { current++ }
Finally, run your test again and it will pass.
The high score should also increment when you score, so add the following test to ScoreUnitTests
:
@Test fun whenIncrementingScore_aboveHighScore_shouldAlsoIncrementHighScore() { val score = Score() score.increment() Assert.assertEquals(1, score.highest) }
Run the test and it won't pass.
So, modify increment()
in Score
with this:
fun increment() { current++ if (current > highest) { highest = current } }
Run your tests again and they will pass.
More Faking
Currently your app doesn't do anything if you try to answer a question. That's because you haven't implemented answer()
in Game
. Open GameUnitTests
and add the following:
// 1 fun createQuestion(answerReturn: Boolean): Question { return object : Question("", ""){ override fun answer(answer: String): Boolean{ return answerReturn } } } @Test fun whenAnsweringCorrectly_shouldIncrementCurrentScore() { // 2 val question = createQuestion(true) val score = Score() val game = Game(listOf(question), score) game.answer(question, "OPTION") // 3 Assert.assertEquals(score.current, 1) }
Here's what's happening:
- It's creating a helper method to generate a fake of a
Question
object that returnstrue
orfalse
foranswer()
based on what you pass into it. - Next, it sets up a test for answering a question incorrectly.
- Finally, it asserts that a wrong answer doesn't increment the score of the game.
You need to import these:
import com.kodeco.cocktails.game.model.Question import com.kodeco.cocktails.game.model.Score
To fix it replace answer()
from Game
in game ▸ model with:
fun answer(question: Question, option: String) { score.increment() }
Now, run your test again and it'll pass.
Fixing Edge Cases
Currently, this code will increment your score even if your answer is incorrect. To fix that, first, open GameUnitTests
and add the following test:
@Test fun whenAnsweringIncorrectly_shouldNotIncrementCurrentScore() { val question = createQuestion(false) val score = Score() val game = Game(listOf(question), score) game.answer(question, "OPTION") Assert.assertEquals(score.current, 0) }
Next, run it, and your new test will fail.
To fix it replace answer()
in Game
with the following:
fun answer(question: Question, option: String) { val result = question.answer(option) if (result) { score.increment() } }
The code above, only increments the score if the answer is correct.
Finally, run your test again and it'll pass.
Run your app again, now you can answer questions and see the score increment if you guess the correct name for the cocktail!
Wrapping Up
Congratulations! Now that you've learned the basics of unit testing, let's review some of the key concepts.
Where to Go From Here?
Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.
Unit testing takes a lot of time to master and is part of a pyramid of larger tests that help to increase the quality of your apps.
Try practicing some tests on your own in the app:
-
Question
currently doesn't have any unit tests. Try adding some tests to it to cover the happy paths and edge cases. -
answerQuestion()
inCocktailsViewModel
currently doesn't have any test coverage. Try applying the skills you learned here to put that method under test.
Here are some resources to help with your testing journey:
- Mocks aren't stubs: You'll commonly hear in the jargon "You should mock that," but they aren't always strictly referring to mocks. An article from Martin Fowler explains the difference.
- Dependency injection: To make your app more testable, it's good to have your dependencies injected somehow. This Dagger 2 tutorial or this Koin tutorial will help you with that.
- Test patterns: Because writing tests is a bit of an art form, this book from Gerard Meszaros will explain some great patterns to you. It's an incredible reference.
- Compose UI Testing: If you're wondering how UI tests are done, this codelab from Google will help you get started.
For even more testing practice, check out our book, Android Test-Driven Development by Tutorials.
We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!