5.
Unit Tests
Written by Fernando Sproviero
As mentioned in Chapter 4, “The Testing Pyramid,” unit tests verify how isolated parts of your application work. Before checking how things work together, you need to make sure the units of your application behave as expected.
In this chapter, you’ll:
- Learn what unit tests are and what are the best places to use them.
- Write unit tests using the test-driven development (TDD) pattern to learn these concepts in the context of TDD.
Throughout this chapter and Chapter 7, “Introduction to Mockito” you’ll work on an application named Cocktail Game. With this application, you’ll have fun with a trivia game about cocktails.
Find the starter project for this application in the materials for this chapter and open it in Android Studio. Build and run the application. You’ll see a blank screen.
You’ll start writing tests and classes for the application and, by the end of Chapter 7, “Introduction to Mockito,” the application will look like this:
When to use unit tests
Unit tests are the fastest and easiest tests to write. They also are the quickest to run. When you want to ensure that a class or method is working as intended in isolation — this means with no other dependent classes — you write unit tests.
Before writing any feature code, you should first write a unit test for one of the classes that will compose your feature. Afterwards, you write the class that will pass the test. After repeating this procedure, you’ll have a completed, testable feature.
Setting up JUnit
You’re going to write a unit test for the first class of the cocktail game named Game
. This first test will be a JUnit test, so, open app/build.gradle and add the following dependency:
dependencies {
...
testImplementation 'junit:junit:4.13.2'
}
Notice that it’s testImplementation
instead of implementation
because you’ll use this dependency only when testing. This means that it won’t be bundled into the application (APK) that your device or emulator will run.
Note: When creating a new project, you’ll find that this dependency is already there. You’re adding it here manually for educational purposes.
Creating unit tests
To start, switch to the Project View and open app ‣ src. Create a new directory and enter: test/java/com/raywenderlich/android/cocktails/game/model. This creates a new test package for your Game
class. Then, create a file called GameUnitTests.kt.
Write the following code:
class GameUnitTests {
// 1
@Test
fun whenIncrementingScore_shouldIncrementCurrentScore() {
// 2
val game = Game()
// 3
game.incrementScore()
// 4
Assert.assertEquals(1, game.currentScore)
}
}
Note: When importing
Assert
, you should chooseorg.junit.Assert
.
- Notice the
@Test
annotation. This will tell JUnit that this method is a test. - Create an instance of the
Game
class — the one that will be tested. - Call the method that you want to test.
-
assertEquals
verifies that the previous execution modified thegame.currentScore
property to be equal to one. It’s important to understand that the first parameter is the expected value, and the second parameter is the actual value.
There’s also the possibility to write a message so that, when the test fails, you’ll see this message. For example:
Assert.assertEquals("Current score should have been 1",
1, game.currentScore)
Every test has the following steps:
- Set Up: You first have a phase where you arrange, configure or set up; in this case, you instantiate a class.
- Assertion: You execute the method that you want to test and you assert the result.
- Teardown: You want tests to start with the same state each time they are run. Otherwise you might get flaky tests. Sometimes (not in this example), you’ll reset the test state after the tests are done running. This is where it would happen.
If you try to compile the test now, you’ll get this:
Making the test compile
The test won’t compile because the Game
class doesn’t exist. So, create the Game
class under the directory app ‣ src ‣ main ‣ java ‣ com ‣ raywenderlich ‣ android ‣ cocktails ‣ game ‣ model. You’ll need to create game
and model
packages first. In the Game
class, write the minimum amount of code to make the test compile:
class Game() {
var currentScore = 0
private set
fun incrementScore() {
// No implementation yet
}
}
Running the test
Now, go back to the test, import the Game
model and you’ll see that the test now compiles.
There are several ways to run the tests.
You can click the Play button over a test:
You can also use the shortcut ^ + ⇧ + R.
Or, if you want to run all the tests (currently you have just one), you can right-click over the app ‣ src ‣ test ‣ java ‣ com ‣ raywenderlich ‣ android ‣ cocktails ‣ game ‣ model package and select Run ‘Tests’ in ‘model’:
Either way, you should see that it doesn’t pass:
This is because you didn’t increment the currentScore
yet. You’ll fix that soon.
You can also open the Terminal going to View ‣ Tool Windows ‣ Terminal and run the tests from the command line executing:
$ ./gradlew test
Notice how the expected value is one and the actual value is zero. If we had reversed the order of our expected and actual values in our assertion, this would show up incorrectly.
You’ll also see that it generates a report under /app/build/reports/tests/testDebugUnitTest/index.html; if you open it in your preferred browser, you’ll see the following:
Making the test pass
Modify the Game
class to make it pass:
class Game() {
var currentScore = 0
private set
fun incrementScore() {
currentScore++
}
}
Now run the test again and see that it passes.
Or if you run the command in the Terminal:
$ ./gradlew test
It’ll generate this report:
Creating more tests
The game will show a highest score. So, you should add a test that checks that when the current score is above the highest score, it increments the highest score:
@Test
fun whenIncrementingScore_aboveHighScore_shouldAlsoIncrementHighScore() {
val game = Game()
game.incrementScore()
Assert.assertEquals(1, game.highestScore)
}
Again if you try to compile it’ll fail because the highestScore
property is missing.
So, add the following property to the Game
class:
var highestScore = 0
private set
Now the test will compile, so run it and watch it fail.
To make it pass, open the Game
class and modify the incrementScore()
method as follows:
fun incrementScore() {
currentScore++
highestScore++
}
Run the test and you’ll see that it passes.
However, you should also test that, when the highest score is greater than the current score, incrementing the current score won’t also increment the highest score, so add the following test:
@Test
fun whenIncrementingScore_belowHighScore_shouldNotIncrementHighScore() {
val game = Game(10)
game.incrementScore()
Assert.assertEquals(10, game.highestScore)
}
Here, the intention is to create a Game
with a highscore of 10. The test won’t compile because you need to modify the constructor to allow a parameter. Because you need to start with a highest score greater than the default, which is 0, you need to alter the constructor like this:
class Game(highest: Int = 0) {
And change the highestScore
property to be set to highest
:
var highestScore = highest
private set
Now, run all the tests and see that the last one doesn’t pass. You can use the green arrow button on the left-side of the class definition.
The last one doesn’t pass because you’re incrementing both the current score and highest score regardless of their values. Fix that by replacing the incrementScore()
function with the following:
fun incrementScore() {
currentScore++
if (currentScore > highestScore) {
highestScore = currentScore
}
}
Build and run the last test to see the satisfying green checkmark.
JUnit annotations
For this project, you’re creating a trivia game. Trivias have questions, so you’ll now create unit tests that model a question with two possible answers. The question also has an “answered” option to model what the user has answered to the question. Create a file called QuestionUnitTests.kt in the app ‣ src ‣ test ‣ java ‣ com ‣ raywenderlich ‣ android ‣ cocktails ‣ game ‣ model directory.
Add the following code:
class QuestionUnitTests {
@Test
fun whenCreatingQuestion_shouldNotHaveAnsweredOption() {
val question = Question("CORRECT", "INCORRECT")
Assert.assertNull(question.answeredOption)
}
}
Here, you used assertNull
to check if question.answeredOption
is null
.
If you try to run this test it won’t compile because the Question
class doesn’t exist.
Create the Question
class under the directory app ‣ src ‣ main ‣ java ‣ com ‣ raywenderlich ‣ android ‣ cocktails ‣ game ‣ model and add the following to make it compile:
class Question(val correctOption: String,
val incorrectOption: String) {
var answeredOption: String? = "MY ANSWER"
private set
}
Run the test again and watch it fail.
It failed because you hardcoded "MY ANSWER"
which is not null.
Modify the Question
class to the following:
class Question(val correctOption: String,
val incorrectOption: String) {
var answeredOption: String? = null
private set
}
Run the test again and watch that it now passes.
Now, you can add another test:
@Test
fun whenAnswering_shouldHaveAnsweredOption() {
val question = Question("CORRECT", "INCORRECT")
question.answer("INCORRECT")
Assert.assertEquals("INCORRECT", question.answeredOption)
}
This test will check that, when you add the user’s answer to a question, the user’s answer is saved in the answeredOption
property.
You’ll get a compilation error since you haven’t written the answer()
method yet.
Add the following to the Question
class to make it compile:
fun answer(option: String) {
// No implementation yet
}
Now run the test and you’ll see that it doesn’t pass.
So add the following to the answer()
method:
fun answer(option: String) {
answeredOption = option
}
Run it and watch that it passes.
Because you’ll need to know if the question was answered correctly, imagine that the answer()
method now returns a Boolean
. The result would be true
when the user answered correctly. Now, add this test:
@Test
fun whenAnswering_withCorrectOption_shouldReturnTrue() {
val question = Question("CORRECT", "INCORRECT")
val result = question.answer("CORRECT")
Assert.assertTrue(result)
}
Notice, here, that you’re using assertTrue
. It checks for a Boolean
result.
Running this test will get you a compilation error since the answer()
method doesn’t return a Boolean
. Update the Question
class so that the answer()
method returns a Boolean
.
For now, always return false
:
fun answer(option: String): Boolean {
answeredOption = option
return false
}
Run it and watch it fail.
Fix it temporarily by always returning true
:
fun answer(option: String): Boolean {
answeredOption = option
return true
}
Run it and watch it pass.
Add the following test:
@Test
fun whenAnswering_withIncorrectOption_shouldReturnFalse() {
val question = Question("CORRECT", "INCORRECT")
val result = question.answer("INCORRECT")
Assert.assertFalse(result)
}
Run it and see that it fails.
Now that we have tests for when the answer is correct and when the answer is not correct, we can fix the code:
fun answer(option: String): Boolean {
answeredOption = option
return correctOption == answeredOption
}
Run all the Question
tests and verify they all pass correctly.
Finally, you should ensure that the answer()
method only allows valid options. Add this test:
@Test(expected = IllegalArgumentException::class)
fun whenAnswering_withInvalidOption_shouldThrowException() {
val question = Question("CORRECT", "INCORRECT")
question.answer("INVALID")
}
Notice, here, that the @Test
annotation allows to expect an exception. If that exception occurs, the test will pass. This will save you from writing try/catch
. If you run the test now, it will fail because the answer()
method doesn’t throw the exception:
To fix this, modify the Question
class as follows:
fun answer(option: String): Boolean {
if (option != correctOption && option != incorrectOption)
throw IllegalArgumentException("Not a valid option")
answeredOption = option
return correctOption == answeredOption
}
Run the test and watch that it now passes.
Because later you’ll need a property isAnsweredCorrectly
, open the Question
class and refactor to the following:
val isAnsweredCorrectly: Boolean
get() = correctOption == answeredOption
fun answer(option: String): Boolean {
if (option != correctOption && option != incorrectOption)
throw IllegalArgumentException("Not a valid option")
answeredOption = option
return isAnsweredCorrectly
}
Run all the tests again to see that everything is still working after the refactor.
Refactoring the unit tests
Notice that each test repeats this line of code:
val question = Question("CORRECT", "INCORRECT")
This makes the tests bloated with boilerplate code that makes them hard to read. To improve this, JUnit tests can have a method annotated with @Before. This method will be executed before each test and it’s a good place to set up objects.
Modify the QuestionUnitTests test class, adding the following to the top:
private lateinit var question: Question
@Before
fun setup() {
question = Question("CORRECT", "INCORRECT")
}
And remove the repeated line of each test. When you’re done, your tests should look like:
@Test
fun whenCreatingQuestion_shouldNotHaveAnsweredOption() {
Assert.assertNull(question.answeredOption)
}
@Test
fun whenAnswering_shouldHaveAnsweredOption() {
question.answer("INCORRECT")
Assert.assertEquals("INCORRECT", question.answeredOption)
}
@Test
fun whenAnswering_withCorrectOption_shouldReturnTrue() {
val result = question.answer("CORRECT")
Assert.assertTrue(result)
}
@Test
fun whenAnswering_withIncorrectOption_shouldReturnFalse() {
val result = question.answer("INCORRECT")
Assert.assertFalse(result)
}
@Test(expected = IllegalArgumentException::class)
fun whenAnswering_withInvalidOption_shouldThrowException() {
question.answer("INVALID")
}
Now, run all the tests again to make sure you didn’t break them while refactoring. All tests still pass — great!
JUnit also has other similar annotations:
- @After: The method will be executed after each test. You can use it to tear down anything or reset any objects that you set up in @Before.
- @BeforeClass: If you annotate a method with this, it’ll be executed only once before all the tests are executed. For example, opening a file, a connection or a database that is shared in all the tests.
- @AfterClass: Use this one to execute a method only once after all the tests are executed. For example, closing a file, a connection or a database that is shared in all the tests.
Challenge
Challenge: Testing questions
You have the Game
and Question
classes. The Game
class should contain a list of questions. For now, these are the requirements:
- The game should have a list of questions. It should have a
nextQuestion()
method that returns the next question in from the list. - When getting the next question, if you’ve reached the end of the list, the game should return
null
. - The question should have a
getOptions()
method that returns the correct and incorrect options as a shuffled list, so later you can show them as Buttons. Hint: The method should receive a lambda parameter to sort the list, by default it should be{ it.shuffled() }
but having a parameter will let you use another one in your test.
Write a test for each one and add the corresponding functionality to the Game
class progressively to make each test pass.
Remember the TDD procedure: write a test, see it fail, write the minimum amount of code to make it pass and refactor if needed.
Key points
- Unit tests verify how isolated parts of your application work.
- Using JUnit, you can write unit tests asserting results, meaning, you can compare an expected result with the actual one.
- Every test has three phases: set up, assertion and teardown.
- In TDD, you start by writing a test. You then write the code to make the test compile. Next you see that the test fails. Finally, you add the implementation to the method under test to make it pass.
Where to go from here?
Great! You’ve just learned the basics of unit testing with JUnit. You can check the project materials for the final version of the code for this chapter.
In the next chapter, “Architecting for Testing,” you’ll learn about good practices and design patterns, that will ensure a good architecture and encourage testability. Afterwards, you’ll continue working on this project, creating unit tests using a complementary library called Mockito.
For additional resources, there’s a Google library you can use called Truth, similar to JUnit. It has a couple of notable benefits:
- More readable test assertions
- Default failure messages
You can check it out, here: https://google.github.io/truth/comparison