7
Introduction to Mockito
Written by Fernando Sproviero
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
You’ll often find yourself in situations in which you want to write a test for a method of a class that requires collaboration from another class. Unit Tests normally focus on a single class, therefore you need a way to avoid using their actual collaborators. Otherwise, you’d be doing integration testing, which you’ll see in Chapter 8, “Integration.”
In this chapter, you’ll:
- Learn what mocking and stubbing are and when to use these techniques.
- Write more unit tests using the test-driven development (TDD) pattern to continue testing state, and a way to also verify behavior.
Why Mockito?
If you remember from a previous chapter, whenever you create a test, you must:
- First, configure what you’re going to test.
- Second, execute the method that you want to test.
- Finally, verify the result by checking the state of the object under test. This is called state verification or black-box testing. This is what you’ve done using JUnit.
However, to perform state verification, sometimes the object under test has to collaborate with another one. Because you want to focus on the first object, in the configuration phase, you want to provide a test double collaborator to your object under test. This fake collaborator is just for testing purposes and you configure it to behave as you want. For example, you could make a mock so that calling a method on it always returns the same hardcoded String
. This is called stubbing a method. You’ll use Mockito for this.
There’s another type of verification called behavior verification or white-box testing. Here you want to ensure that your object under test will call specific collaborator methods. For example, you may have a repository object that retrieves the data from the network, and before returning the results, it calls a collaborator object to save them into a database. Again, you can use Mockito to keep an eye on a collaborator and verify if specific methods were called on it.
Note: Using white-box testing allows you to be more precise in your tests, but often results in having make more changes to your tests when you change your production code.
Setting up Mockito
Open the application’s build.gradle file and add the following dependency:
dependencies {
...
testImplementation 'org.mockito.kotlin:mockito-kotlin:3.2.0'
}
Creating unit tests with Mockito
Later, in the UI, you’ll show the user a question with two options. You’ll want the user to click on one. Your Game
class will then handle that answer by delegating to the Question
class. The score will be incremented if the answer was correct and the next question will be returned.
Mocking and verifying
Start by adding the following test to the GameUnitTests.kt file:
@Test
fun whenAnswering_shouldDelegateToQuestion() {
// 1
val question = mock<Question>()
val game = Game(listOf(question))
// 2
game.answer(question, "OPTION")
// 3
verify(question, times(1)).answer(eq("OPTION"))
}
verify(question).answer(eq("OPTION"))
fun answer(question: Question, option: String) {
// TODO
}
Using mock-maker-inline extension
- Go to the project window and change to the Project view.
- Create a resources directory under app ‣ src ‣ test.
- Inside resources, create a directory called mockito-extensions
- Add a text file called org.mockito.plugins.MockMaker
- Add the text mock-maker-inline
fun answer(question: Question, option: String) {
question.answer(option)
}
Stubbing methods
The Game
should increment the current score when answered correctly, so add the following test:
@Test
fun whenAnsweringCorrectly_shouldIncrementCurrentScore() {
// 1
val question = mock<Question>()
whenever(question.answer(anyString())).thenReturn(true)
val game = Game(listOf(question))
// 2
game.answer(question, "OPTION")
// 3
Assert.assertEquals(1, game.currentScore)
}
fun answer(question: Question, option: String) {
question.answer(option)
incrementScore()
}
@Test
fun whenAnsweringIncorrectly_shouldNotIncrementCurrentScore() {
val question = mock<Question>()
whenever(question.answer(anyString())).thenReturn(false)
val game = Game(listOf(question))
game.answer(question, "OPTION")
Assert.assertEquals(0, game.currentScore)
}
fun answer(question: Question, option: String) {
val result = question.answer(option)
if (result) {
incrementScore()
}
}
Refactoring
Open the Game
class. Notice that this class knows about the score and a list of questions. When requesting to answer a question, the Game
class delegates this to the Question
class and increments the score if the answer was correct. Game
could also be refactored to delegate the logic of incrementing the current score and highest score to a new class, Score
.
class Score(highestScore: Int = 0) {
var current = 0
private set
var highest = highestScore
private set
fun increment() {
current++
if (current > highest) {
highest = current
}
}
}
class Game(private val questions: List<Question>,
highest: Int = 0) {
private val score = Score(highest)
val currentScore: Int
get() = score.current
val highestScore: Int
get() = score.highest
private var questionIndex = -1
fun incrementScore() {
score.increment()
}
...
@Test
fun whenIncrementingScore_shouldIncrementCurrentScore() {
val game = Game(emptyList(), 0)
game.incrementScore()
Assert.assertEquals(
"Current score should have been 1",
1,
game.currentScore)
}
@Test
fun whenIncrementingScore_aboveHighScore_shouldAlsoIncrementHighScore() {
val game = Game(emptyList(), 0)
game.incrementScore()
Assert.assertEquals(1, game.highestScore)
}
@Test
fun whenIncrementingScore_belowHighScore_shouldNotIncrementHighScore() {
val game = Game(emptyList(), 10)
game.incrementScore()
Assert.assertEquals(10, game.highestScore)
}
class ScoreUnitTests {
@Test
fun whenIncrementingScore_shouldIncrementCurrentScore() {
val score = Score()
score.increment()
Assert.assertEquals(
"Current score should have been 1",
1,
score.current)
}
@Test
fun whenIncrementingScore_aboveHighScore_shouldAlsoIncrementHighScore() {
val score = Score()
score.increment()
Assert.assertEquals(1, score.highest)
}
@Test
fun whenIncrementingScore_belowHighScore_shouldNotIncrementHighScore() {
val score = Score(10)
score.increment()
Assert.assertEquals(10, score.highest)
}
}
fun answer(question: Question, option: String) {
val result = question.answer(option)
if (result) {
score.increment()
}
}
@Test
fun whenAnsweringCorrectly_shouldIncrementCurrentScore() {
val question = mock<Question>()
whenever(question.answer(anyString())).thenReturn(true)
val game = Game(listOf(question))
game.answer(question, "OPTION")
Assert.assertEquals(1, game.currentScore)
}
@Test
fun whenAnsweringIncorrectly_shouldNotIncrementCurrentScore() {
val question = mock<Question>()
whenever(question.answer(anyString())).thenReturn(false)
val game = Game(listOf(question))
game.answer(question, "OPTION")
Assert.assertEquals(0, game.currentScore)
}
@Test
fun whenAnsweringCorrectly_shouldIncrementCurrentScore() {
val question = mock<Question>()
whenever(question.answer(anyString())).thenReturn(true)
val score = mock<Score>()
val game = Game(listOf(question), score)
game.answer(question, "OPTION")
verify(score).increment()
}
@Test
fun whenAnsweringIncorrectly_shouldNotIncrementCurrentScore() {
val question = mock<Question>()
whenever(question.answer(anyString())).thenReturn(false)
val score = mock<Score>()
val game = Game(listOf(question), score)
game.answer(question, "OPTION")
verify(score, never()).increment()
}
class Game(private val questions: List<Question>,
val score: Score = Score(0)) {
class Game(private val questions: List<Question>,
val score: Score = Score(0)) {
private var questionIndex = -1
fun nextQuestion(): Question? {
if (questionIndex + 1 < questions.size) {
questionIndex++
return questions[questionIndex]
}
return null
}
fun answer(question: Question, option: String) {
val result = question.answer(option)
if (result) {
score.increment()
}
}
}
Verifying in order
To save and retrieve the high score, you’ll need to add functionality to a repository. From the Project view, create a new package common ‣ repository under app ‣ src ‣ test ‣ java ‣ com ‣ raywenderlich ‣ android ‣ cocktails. Create a new file called RepositoryUnitTests.kt and add the following code:
class RepositoryUnitTests {
@Test
fun saveScore_shouldSaveToSharedPreferences() {
val api: CocktailsApi = mock()
// 1
val sharedPreferencesEditor: SharedPreferences.Editor =
mock()
val sharedPreferences: SharedPreferences = mock()
whenever(sharedPreferences.edit())
.thenReturn(sharedPreferencesEditor)
val repository = CocktailsRepositoryImpl(api,
sharedPreferences)
// 2
val score = 100
repository.saveHighScore(score)
// 3
inOrder(sharedPreferencesEditor) {
// 4
verify(sharedPreferencesEditor).putInt(any(), eq(score))
verify(sharedPreferencesEditor).apply()
}
}
}
interface CocktailsRepository {
...
fun saveHighScore(score: Int)
}
class CocktailsRepositoryImpl(
private val api: CocktailsApi,
private val sharedPreferences: SharedPreferences)
: CocktailsRepository {
override fun saveHighScore(score: Int) {
// TODO
}
private const val HIGH_SCORE_KEY = "HIGH_SCORE_KEY"
class CocktailsRepositoryImpl(
private val api: CocktailsApi,
private val sharedPreferences: SharedPreferences)
: CocktailsRepository {
...
override fun saveHighScore(score: Int) {
val editor = sharedPreferences.edit()
editor.putInt(HIGH_SCORE_KEY, score)
editor.apply()
}
@Test
fun getScore_shouldGetFromSharedPreferences() {
val api: CocktailsApi = mock()
val sharedPreferences: SharedPreferences = mock()
val repository = CocktailsRepositoryImpl(api,
sharedPreferences)
repository.getHighScore()
verify(sharedPreferences).getInt(any(), any())
}
interface CocktailsRepository {
...
fun getHighScore(): Int
}
class CocktailsRepositoryImpl(
private val api: CocktailsApi,
private val sharedPreferences: SharedPreferences)
: CocktailsRepository {
...
override fun getHighScore(): Int = 0
override fun getHighScore()
= sharedPreferences.getInt(HIGH_SCORE_KEY, 0)
class RepositoryUnitTests {
private lateinit var repository: CocktailsRepository
private lateinit var api: CocktailsApi
private lateinit var sharedPreferences: SharedPreferences
private lateinit var sharedPreferencesEditor: SharedPreferences.Editor
@Before
fun setup() {
api = mock()
sharedPreferences = mock()
sharedPreferencesEditor = mock()
whenever(sharedPreferences.edit())
.thenReturn(sharedPreferencesEditor)
repository = CocktailsRepositoryImpl(api, sharedPreferences)
}
@Test
fun saveScore_shouldSaveToSharedPreferences() {
val score = 100
repository.saveHighScore(score)
inOrder(sharedPreferencesEditor) {
verify(sharedPreferencesEditor).putInt(any(), eq(score))
verify(sharedPreferencesEditor).apply()
}
}
@Test
fun getScore_shouldGetFromSharedPreferences() {
repository.getHighScore()
verify(sharedPreferences).getInt(any(), any())
}
}
Spying
Suppose you want to only save the high score if it is higher than the previously saved high score. To do that, you want to start by adding the following test to your RepositoryUnitTests
class:
@Test
fun saveScore_shouldNotSaveToSharedPreferencesIfLower() {
val previouslySavedHighScore = 100
val newHighScore = 10
val spyRepository = spy(repository)
doReturn(previouslySavedHighScore)
.whenever(spyRepository)
.getHighScore()
spyRepository.saveHighScore(newHighScore)
verify(sharedPreferencesEditor, never())
.putInt(any(), eq(newHighScore))
}
override fun saveHighScore(score: Int) {
val highScore = getHighScore()
if (score > highScore) {
val editor = sharedPreferences.edit()
editor.putInt(HIGH_SCORE_KEY, score)
editor.apply()
}
}
class CocktailsGameFactoryUnitTests {
private lateinit var repository: CocktailsRepository
private lateinit var factory: CocktailsGameFactory
@Before
fun setup() {
repository = mock()
factory = CocktailsGameFactoryImpl(repository)
}
@Test
fun buildGame_shouldGetCocktailsFromRepo() {
factory.buildGame(mock())
verify(repository).getAlcoholic(any())
}
}
interface CocktailsGameFactory {
fun buildGame(callback: Callback)
interface Callback {
fun onSuccess(game: Game)
fun onError()
}
}
class CocktailsGameFactoryImpl(
private val repository: CocktailsRepository)
: CocktailsGameFactory {
override fun buildGame(callback: CocktailsGameFactory.Callback) {
// TODO
}
}
override fun buildGame(callback: CocktailsGameFactory.Callback) {
repository.getAlcoholic(
object : RepositoryCallback<List<Cocktail>, String> {
override fun onSuccess(cocktailList: List<Cocktail>) {
// TODO
}
override fun onError(e: String) {
// TODO
}
})
}
Stubbing callbacks
Create a new test that verifies that the callback is called when the repository returns successfully with a list of cocktails in CocktailsGameFactoryUnitTests.kt:
private val cocktails = listOf(
Cocktail("1", "Drink1", "image1"),
Cocktail("2", "Drink2", "image2"),
Cocktail("3", "Drink3", "image3"),
Cocktail("4", "Drink4", "image4")
)
@Test
fun buildGame_shouldCallOnSuccess() {
val callback = mock<CocktailsGameFactory.Callback>()
setUpRepositoryWithCocktails(repository)
factory.buildGame(callback)
verify(callback).onSuccess(any())
}
private fun setUpRepositoryWithCocktails(
repository: CocktailsRepository) {
doAnswer {
// 1
val callback: RepositoryCallback<List<Cocktail>, String>
= it.getArgument(0)
callback.onSuccess(cocktails)
}.whenever(repository).getAlcoholic(any())
}
override fun buildGame(callback: CocktailsGameFactory.Callback) {
repository.getAlcoholic(
object : RepositoryCallback<List<Cocktail>, String> {
override fun onSuccess(cocktailList: List<Cocktail>) {
callback.onSuccess(Game(emptyList()))
}
override fun onError(e: String) {
// TODO
}
})
}
@Test
fun buildGame_shouldCallOnError() {
val callback = mock<CocktailsGameFactory.Callback>()
setUpRepositoryWithError(repository)
factory.buildGame(callback)
verify(callback).onError()
}
private fun setUpRepositoryWithError(
repository: CocktailsRepository) {
doAnswer {
val callback: RepositoryCallback<List<Cocktail>, String>
= it.getArgument(0)
callback.onError("Error")
}.whenever(repository).getAlcoholic(any())
}
override fun buildGame(
callback: CocktailsGameFactory.Callback
) {
repository.getAlcoholic(
object : RepositoryCallback<List<Cocktail>, String> {
override fun onSuccess(cocktailList: List<Cocktail>) {
callback.onSuccess(Game(emptyList()))
}
override fun onError(e: String) {
callback.onError()
}
})
}
@Test
fun buildGame_shouldGetHighScoreFromRepo() {
setUpRepositoryWithCocktails(repository)
factory.buildGame(mock())
verify(repository).getHighScore()
}
@Test
fun buildGame_shouldBuildGameWithHighScore() {
setUpRepositoryWithCocktails(repository)
val highScore = 100
whenever(repository.getHighScore()).thenReturn(highScore)
factory.buildGame(object : CocktailsGameFactory.Callback {
override fun onSuccess(game: Game)
= Assert.assertEquals(highScore, game.score.highest)
override fun onError() = Assert.fail()
})
}
override fun buildGame(callback: CocktailsGameFactory.Callback) {
repository.getAlcoholic(
object : RepositoryCallback<List<Cocktail>, String> {
override fun onSuccess(cocktailList: List<Cocktail>) {
val score = Score(repository.getHighScore())
val game = Game(emptyList(), score)
callback.onSuccess(game)
}
override fun onError(e: String) {
callback.onError()
}
})
}
@Test
fun buildGame_shouldBuildGameWithQuestions() {
setUpRepositoryWithCocktails(repository)
factory.buildGame(object : CocktailsGameFactory.Callback {
override fun onSuccess(game: Game) {
cocktails.forEach {
assertQuestion(game.nextQuestion(),
it.strDrink,
it.strDrinkThumb)
}
}
override fun onError() = Assert.fail()
})
}
private fun assertQuestion(question: Question?,
correctOption: String,
imageUrl: String?) {
Assert.assertNotNull(question)
Assert.assertEquals(imageUrl, question?.imageUrl)
Assert.assertEquals(correctOption, question?.correctOption)
Assert.assertNotEquals(correctOption,
question?.incorrectOption)
}
class Question(val correctOption: String,
val incorrectOption: String,
val imageUrl: String? = null) {
...
override fun buildGame(callback: CocktailsGameFactory.Callback) {
repository.getAlcoholic(
object : RepositoryCallback<List<Cocktail>, String> {
override fun onSuccess(cocktailList: List<Cocktail>) {
val questions = buildQuestions(cocktailList)
val score = Score(repository.getHighScore())
val game = Game(questions, score)
callback.onSuccess(game)
}
override fun onError(e: String) {
callback.onError()
}
})
}
private fun buildQuestions(cocktailList: List<Cocktail>)
= cocktailList.map { cocktail ->
val otherCocktail
= cocktailList.shuffled().first { it != cocktail }
Question(cocktail.strDrink,
otherCocktail.strDrink,
cocktail.strDrinkThumb)
}
Testing ViewModel and LiveData
To update the UI with questions, the score, and also to enable the user to interact with the question options, you’re going to use ViewModel
and LiveData
from Android Architecture Components.
dependencies {
...
testImplementation 'androidx.arch.core:core-testing:2.1.0'
}
class CocktailsGameViewModelUnitTests {
@get:Rule
val taskExecutorRule = InstantTaskExecutorRule()
}
private lateinit var repository: CocktailsRepository
private lateinit var factory: CocktailsGameFactory
private lateinit var viewModel: CocktailsGameViewModel
private lateinit var game: Game
private lateinit var loadingObserver: Observer<Boolean>
private lateinit var errorObserver: Observer<Boolean>
private lateinit var scoreObserver: Observer<Score>
private lateinit var questionObserver: Observer<Question>
@Before
fun setup() {
// 1
repository = mock()
factory = mock()
viewModel = CocktailsGameViewModel(repository, factory)
// 2
game = mock()
// 3
loadingObserver = mock()
errorObserver = mock()
scoreObserver = mock()
questionObserver = mock()
viewModel.getLoading().observeForever(loadingObserver)
viewModel.getScore().observeForever(scoreObserver)
viewModel.getQuestion().observeForever(questionObserver)
viewModel.getError().observeForever(errorObserver)
}
class CocktailsGameViewModel(
private val repository: CocktailsRepository,
private val factory: CocktailsGameFactory) : ViewModel() {
private val loadingLiveData = MutableLiveData<Boolean>()
private val errorLiveData = MutableLiveData<Boolean>()
private val questionLiveData = MutableLiveData<Question>()
private val scoreLiveData = MutableLiveData<Score>()
fun getLoading(): LiveData<Boolean> = loadingLiveData
fun getError(): LiveData<Boolean> = errorLiveData
fun getQuestion(): LiveData<Question> = questionLiveData
fun getScore(): LiveData<Score> = scoreLiveData
}
private fun setUpFactoryWithSuccessGame(game: Game) {
doAnswer {
val callback: CocktailsGameFactory.Callback =
it.getArgument(0)
callback.onSuccess(game)
}.whenever(factory).buildGame(any())
}
private fun setUpFactoryWithError() {
doAnswer {
val callback: CocktailsGameFactory.Callback =
it.getArgument(0)
callback.onError()
}.whenever(factory).buildGame(any())
}
@Test
fun init_shouldBuildGame() {
viewModel.initGame()
verify(factory).buildGame(any())
}
fun initGame() {
// TODO
}
fun initGame() {
factory.buildGame(object : CocktailsGameFactory.Callback {
override fun onSuccess(game: Game) {
// TODO
}
override fun onError() {
// TODO
}
})
}
@Test
fun init_shouldShowLoading() {
viewModel.initGame()
verify(loadingObserver).onChanged(eq(true))
}
@Test
fun init_shouldHideError() {
viewModel.initGame()
verify(errorObserver).onChanged(eq(false))
}
fun initGame() {
loadingLiveData.value = true
errorLiveData.value = false
factory.buildGame(...)
}
@Test
fun init_shouldShowError_whenFactoryReturnsError() {
setUpFactoryWithError()
viewModel.initGame()
verify(errorObserver).onChanged(eq(true))
}
@Test
fun init_shouldHideLoading_whenFactoryReturnsError() {
setUpFactoryWithError()
viewModel.initGame()
verify(loadingObserver).onChanged(eq(false))
}
override fun onError() {
loadingLiveData.value = false
errorLiveData.value = true
}
@Test
fun init_shouldHideError_whenFactoryReturnsSuccess() {
setUpFactoryWithSuccessGame(game)
viewModel.initGame()
verify(errorObserver, times(2)).onChanged(eq(false))
}
@Test
fun init_shouldHideLoading_whenFactoryReturnsSuccess() {
setUpFactoryWithSuccessGame(game)
viewModel.initGame()
verify(loadingObserver).onChanged(eq(false))
}
override fun onSuccess(game: Game) {
loadingLiveData.value = false
errorLiveData.value = false
}
@Test
fun init_shouldShowScore_whenFactoryReturnsSuccess() {
val score = mock<Score>()
whenever(game.score).thenReturn(score)
setUpFactoryWithSuccessGame(game)
viewModel.initGame()
verify(scoreObserver).onChanged(eq(score))
}
override fun onSuccess(game: Game) {
loadingLiveData.value = false
errorLiveData.value = false
scoreLiveData.value = game.score
}
@Test
fun init_shouldShowFirstQuestion_whenFactoryReturnsSuccess() {
val question = mock<Question>()
whenever(game.nextQuestion()).thenReturn(question)
setUpFactoryWithSuccessGame(game)
viewModel.initGame()
verify(questionObserver).onChanged(eq(question))
}
override fun onSuccess(game: Game) {
loadingLiveData.value = false
errorLiveData.value = false
scoreLiveData.value = game.score
questionLiveData.value = game.nextQuestion()
}
@Test
fun nextQuestion_shouldShowQuestion() {
val question1 = mock<Question>()
val question2 = mock<Question>()
whenever(game.nextQuestion())
.thenReturn(question1)
.thenReturn(question2)
setUpFactoryWithSuccessGame(game)
viewModel.initGame()
viewModel.nextQuestion()
verify(questionObserver).onChanged(eq(question2))
}
fun nextQuestion() {
// TODO
}
fun nextQuestion() {
game?.let {
questionLiveData.value = it.nextQuestion()
}
}
override fun onSuccess(game: Game) {
loadingLiveData.value = false
errorLiveData.value = false
scoreLiveData.value = game.score
this@CocktailsGameViewModel.game = game
nextQuestion()
}
private var game: Game? = null
@Test
fun answerQuestion_shouldDelegateToGame_saveHighScore_showQuestionAndScore() {
val score = mock<Score>()
val question = mock<Question>()
whenever(game.score).thenReturn(score)
setUpFactoryWithSuccessGame(game)
viewModel.initGame()
viewModel.answerQuestion(question, "VALUE")
inOrder(game, repository, questionObserver, scoreObserver) {
verify(game).answer(eq(question), eq("VALUE"))
verify(repository).saveHighScore(any())
verify(scoreObserver).onChanged(eq(score))
verify(questionObserver).onChanged(eq(question))
}
}
fun answerQuestion(question: Question, option: String) {
}
fun answerQuestion(question: Question, option: String) {
game?.let {
it.answer(question, option)
repository.saveHighScore(it.score.highest)
scoreLiveData.value = it.score
questionLiveData.value = question
}
}
Mockito annotations
Instead of calling the mock()
and spy()
methods, you can use annotations. For example, open RepositoryUnitTests.kt and modify the class definition, variable definitions and setup functions to look like the following:
@RunWith(MockitoJUnitRunner::class)
class RepositoryUnitTests {
private lateinit var repository: CocktailsRepository
@Mock
private lateinit var api: CocktailsApi
@Mock
private lateinit var sharedPreferences: SharedPreferences
@Mock
private lateinit var sharedPreferencesEditor:
SharedPreferences.Editor
@Before
fun setup() {
whenever(sharedPreferences.edit())
.thenReturn(sharedPreferencesEditor)
repository = CocktailsRepositoryImpl(api, sharedPreferences)
}
Challenge
Challenge: Writing another test
- When answering incorrectly three times, it should finish the game.
- When answering correctly three times sequentially, it should start giving double score.
Key points
- With JUnit you can do state verification, also called black-box testing.
- With Mockito you can perform behavior verification or white-box testing.
- Using a mock of a class will let you stub methods simulate a particular situation in a test. It’ll also verify if one or more methods were called on that mock.
- Using a spy is similar to using a mock, but on real instances. You’ll be able to stub a method and verify if a method was called just like a mock, but also be able to call the real methods of the instance.
- Remember: Red, Green, Refactor
Where to go from here?
Awesome! You’ve just learned the basics of unit testing with Mockito.