Chapters

Hide chapters

Android Test-Driven Development by Tutorials

Second Edition · Android 11 · Kotlin 1.5 · Android Studio 4.2.1

Section II: Testing on a New Project

Section 2: 8 chapters
Show chapters Hide chapters

Section III: TDD on Legacy Projects

Section 3: 8 chapters
Show chapters Hide chapters

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:

Game Screen
Game Screen

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 choose org.junit.Assert.

  1. Notice the @Test annotation. This will tell JUnit that this method is a test.
  2. Create an instance of the Game class — the one that will be tested.
  3. Call the method that you want to test.
  4. assertEquals verifies that the previous execution modified the game.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

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.