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 2 of 3 of this article. Click here to view the first page.

Unit Test Basics

Look at your project structure, in the Android view, you’ll see two references of com.kodeco.cocktails:

Kodeco Cocktails Package

One of them has a (test) next to it. That’s where Android looks for unit tests to run. Switch to the Project view and you’ll see these are in a separate directory called test:

Project View Test Directory

This keeps your unit tests separate from the code you deploy with your app.

Note: You may see a third folder called androidTest, this folder contains tests called instrumentation tests, which will not be covered by this tutorial, but in case you’re curious, this is where you would put your tests for Composables if you wanted to test them.

To start, you’ll write a test for nextQuestion(). Create a file under com.kodeco.cocktails of the test directory called GameUnitTests.kt and paste in the following:

package com.kodeco.cocktails

import com.kodeco.cocktails.game.model.Game
import com.kodeco.cocktails.game.model.QuestionImpl
import org.junit.Assert
import org.junit.Test

class GameUnitTests {
  @Test
  fun whenGettingNextQuestion_shouldReturnIt() {
    // 1
    val question1 = QuestionImpl("CORRECT", "INCORRECT")
    val questions = listOf(question1)
    // 2
    val game = Game(questions)
    // 3
    val nextQuestion = game.nextQuestion()
    // 4
    Assert.assertSame(question1, nextQuestion)
  }
}

This is a class with one test. By annotating whenGettingNextQuestion_shouldReturnIt() with @Test, you’re telling Gradle this is an executable unit test. Your test does the following:

  1. Creates a list of questions with one question.
  2. Constructs a game with the list of questions you created.
  3. Calls nextQuestion() on Game.
  4. Asserts that nextQuestion() returns the same question you passed into your game to initialize it.

Next, click the green arrow next to your method and you’ll see the following options:

Options to Run the Test

Select the first option and your test will run:

Failing Test

Oh no! Your test is currently failing! In this case it’s expected because you haven’t put any logic in your nextQuestion() method implementation. To fix that, replace nextQuestion() from Game with the following:

fun nextQuestion(): Question? {
  questionIndex++
  return questions[questionIndex]
}

Run your test again and it’ll pass.

Passing Tests

Edge Cases

Your implementation of nextQuestion() works, but if you execute this method after iterating through all the questions, you’ll end up getting an index out of bounds exception. To fix that, start by pasting the following test method into GameUnitTests:

@Test
fun whenGettingNextQuestion_withoutMoreQuestions_shouldReturnNull() {
  val question1 = QuestionImpl("CORRECT", "INCORRECT")
  val questions = listOf(question1)
  val game = Game(questions)

  game.nextQuestion()
  val nextQuestion = game.nextQuestion()

  Assert.assertNull(nextQuestion)
}

Next, run the test and it’ll fail.

Failing Test

Now, to fix it, replace nextQuestion() with:

fun nextQuestion(): Question? {
  if (questionIndex + 1 < questions.size) {
    questionIndex++
    return questions[questionIndex]
  }
  return null
}

Finally, run all the tests in the class and they will pass. To do this, click the green arrow button next to the class name.

Passing Tests

Now you have a game model that can return questions for your viewmodel.

You may have noticed you wrote tests before implementing the functionality you were testing and ran the test to make sure it failed. That's by design and is part of a technique called Test Driven Development. The idea behind it is to ensure you don't have a test that won't pass with an incorrect implementation. You may run into situations where you aren't able to write a test before an implementation. If you run into that scenario, it's important to temporarily break the code you're testing to make sure you don't have a false positive.

Testing the ViewModel

So far you've been testing a fairly simple class. Your viewmodel has more going on and will have a more complex unit test. From the vantage of your viewmodel, to be able to provide a question to your composable it, you'll need to do two things. First, you'll need to create a game, and second, you'll need to fetch a question.

To get started, open CocktailsViewModel and you'll see the following:

class CocktailsViewModel(
  private val repository: CocktailsRepository,
  private val factory: CocktailsGameFactory,
  private val dispatcher: CoroutineDispatcher = IO
) : ViewModel() {

There are three things you need to pass to your viewmodel to initialize it:

  1. CocktailsRepository
  2. CocktailsGameFactory
  3. CoroutineDispatcher

CocktailsGameFactory is what you'll use to create a game. Now, open CocktailsGameFactory and you'll see the following interface:

interface CocktailsGameFactory {

  suspend fun buildGame(): RequestState<Game>

}

Your factory has one method called buildGame(). Because you have a suspend function, you'll need to run this in a coroutine scope. Next, open CocktailsRepository and you'll see the following:

interface CocktailsRepository {
  suspend fun getAlcoholic():RequestState<List<Cocktail>>
  suspend fun saveHighScore(score: Int)
  suspend fun getHighScore(): Result<Int?>
}

It's actually an interface; this will come in handy in a minute.

Writing Tests

To get started, you'll need to build your dependencies for your tests. Go to your tests directory, create a file called CocktailsViewModelTests.kt, and paste in the following:

package com.kodeco.cocktails

import com.kodeco.cocktails.game.factory.CocktailsGameFactory
import com.kodeco.cocktails.game.model.Game
import com.kodeco.cocktails.game.model.QuestionImpl
import com.kodeco.cocktails.game.model.RequestState
import com.kodeco.cocktails.game.model.Score
import com.kodeco.cocktails.network.Cocktail
import com.kodeco.cocktails.repository.CocktailsRepository

class CocktailsViewModelTests {

  // 1
  val questions = arrayListOf(
    QuestionImpl("Beer", "Wine"),
    QuestionImpl("Martini", "Amarula")
  )
  // 2
  fun buildGame(): Game {
    return Game(
      questions = questions,
      score = Score(0)
    )
  }
  // 3
  fun buildCocktailsGameFactory(game: Game): CocktailsGameFactory {
    return object : CocktailsGameFactory {
      override suspend fun buildGame(): RequestState<Game> {
        return RequestState.Success(
          game
        )
      }

    }
  }
  // 4
  val fakeRepository = object : CocktailsRepository {
    override suspend fun getAlcoholic(): RequestState<List<Cocktail>> {
      TODO("Not yet implemented")
    }

    override suspend fun saveHighScore(score: Int) {
      TODO("Not yet implemented")
    }

    override suspend fun getHighScore(): Result<Int?> {
      TODO("Not yet implemented")
    }
  }
}

This is doing the following:

  1. It builds a list of test questions.
  2. Creates a helper function to generate a game.
  3. Creates a helper function to create a CocktailsGameFactory.
  4. Generates a fake CocktailsRepository.

Next, you'll need a helper function to create an instance of your viewmodel to test, so paste in the following:

// 1
fun buildSuccessfulGameViewModel(game: Game, testScheduler: TestCoroutineScheduler): CocktailsViewModel {
  // 2
  val cocktailsGameFactorySuccess = buildCocktailsGameFactory(game)
  // 3
  val standardTestDispatcher = StandardTestDispatcher(testScheduler)
  // 4
  return CocktailsViewModel(fakeRepository, cocktailsGameFactorySuccess, standardTestDispatcher)
}

This is doing the following:

  1. Takes a Game and a TestCoroutineScheduler as input.
  2. Builds your fake game factory.
  3. Creates a CoroutineTestDispatcher.
  4. Creates a CocktailsViewModel

Note: You need TestCoroutineScheduler and CoroutineTestDispatcher to synchronize your test threads with the coroutine threads. When you write tests without these, you can end up in a situation where your test thread completes before your coroutine thread does, which can lead to inconsistent test results.

You'll need to import these:

import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler

Now, paste in the following to create your first viewmodel test:

// 1
@get:Rule
val taskExecutorRule = InstantTaskExecutorRule()
// 2
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun init_should_CreateAGame_when_FactoryReturnsSuccess() = runTest {
  val game = buildGame()
  // 3
  val cocktailsViewModel = buildSuccessfulGameViewModel(game, testScheduler)
  cocktailsViewModel.initGame()
  // 4
  advanceUntilIdle()
  // 5
  Assert.assertTrue(cocktailsViewModel.game.value is RequestState.Success)
  val successfulRequest = cocktailsViewModel.game.value as RequestState.Success
  Assert.assertEquals(game, successfulRequest.requestObject)
}

There's a lot going on here:

  1. This is swapping out the background executor used by the Architecture Components with a synchronous thread to make unit tests more predictable.
  2. At the time of this writing, when setting tests that test coroutines, you need to add @OptIn(ExperimentalCoroutinesApi::class).
  3. You're generating a viewmodel for a cocktails game passing in a testScheduler which is provided by the test coroutine helpers in the coroutine libraries.
  4. Because the code you're testing is on a coroutine thread and you're executing it on a synchronous thread, you need to wait for all tasks on the coroutine threat to finish before doing any asserts.
  5. Finally, with everything set up and executed, you can make asserts against the results.

Import the following:

import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.advanceUntilIdle

Next, try running your test.

Failing Test

As expected, it'll fail. To fix it, open CocktailsViewModel and replace initGame() with the following:

fun initGame() {
  viewModelScope.launch(dispatcher) {
    _game.update {
      factory.buildGame()
    }
  }
}

Finally, run your test again and it'll pass.

Passing Test

You now need to display a question when you start a game. Start by pasting the following test into your CocktaislViewModelTests:

@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun init_shouldShowFirstQuestion_whenFactoryReturnsSuccess() = runTest {
  val cocktailsViewModel = buildSuccessfulGameViewModel(buildGame(), testScheduler)
  cocktailsViewModel.initGame()
  advanceUntilIdle()
  val question = cocktailsViewModel.question.value
  Assert.assertEquals(questions.first(), question)
}

Run your tests and this one will fail.

Failing Test

Now, replace initGame() in CocktailsViewModel with:

fun initGame() {
  viewModelScope.launch(dispatcher) {
    _game.update {
      factory.buildGame()
    }
    when (game.value) {
      is RequestState.Success -> {
        _question.update {
          (game.value as RequestState.Success<Game>).requestObject.nextQuestion()
        }
      }

      is RequestState.Error -> {

      }

      else -> {

      }
    }
  }
}

Next, run your tests and they will pass.

Passing Tests

Finally, run your app and you'll see a cocktail question.

Cocktail Question