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
Contents
Unit Testing Tutorial for Android: Getting Started
25 mins
Unit Test Basics
Look at your project structure, in the Android view, you’ll see two references of com.kodeco.cocktails
:
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:
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.
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:
- Creates a list of questions with one question.
- Constructs a game with the list of questions you created.
- Calls
nextQuestion()
onGame
. - 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:
Select the first option and your test will run:
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.
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.
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.
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:
- CocktailsRepository
- CocktailsGameFactory
- 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:
- It builds a list of test questions.
- Creates a helper function to generate a game.
- Creates a helper function to create a
CocktailsGameFactory
. - 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:
- Takes a
Game
and aTestCoroutineScheduler
as input. - Builds your fake game factory.
- Creates a
CoroutineTestDispatcher
. - 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.
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:
- This is swapping out the background executor used by the Architecture Components with a synchronous thread to make unit tests more predictable.
- At the time of this writing, when setting tests that test coroutines, you need to add
@OptIn(ExperimentalCoroutinesApi::class)
. - 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. - 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.
- 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.
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.
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.
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.
Finally, run your app and you'll see a cocktail question.