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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

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.

Failing Test

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.

Failing Test

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.

Passing Test

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.
Failing Test

So, modify increment() in Score with this:

fun increment() {
  current++
  if (current > highest) {
    highest = current
  }
}

Run your tests again and they will pass.
Passing Test

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:

  1. It's creating a helper method to generate a fake of a Question object that returns true or false for answer() based on what you pass into it.
  2. Next, it sets up a test for answering a question incorrectly.
  3. 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

Run your test and it'll fail.
Failing Test

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.

Passing Test

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.

Failing Test

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.

Passing Test

Run your app again, now you can answer questions and see the score increment if you guess the correct name for the cocktail!

Cocktail Question

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() in CocktailsViewModel 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!