Write unit tests

1. Before you begin

In previous codelabs, you learned how to create a project with Android Studio, modify XML to make a customized UI for your app, and modify business logic to add functionality. This codelab focuses on why testing is important and expands upon unit tests. You get the opportunity to see what they look like and how to write them.

Prerequisites

  • You have created a project in Android Studio.
  • You have some experience writing code in Android Studio.

What you'll learn

  • Why testing is important.
  • What unit tests look like.
  • How to write and run unit tests.

What you'll need

  • A computer with Android Studio installed.
  • The project you created in the previous codelab in this pathway.

2. Introduction

Now that you've written some Android code, it's a great time to follow up with some test code. First, you go over some philosophy on testing, then dive deeper into the autogenerated tests in an Android project, and lastly write your own tests for the Dice Roller app! This lesson covers a lot of material, but don't be intimidated! Take your time with this material because testing takes a long time and a lot of practice to learn. Don't be discouraged if you don't get a handle on it right away.

Why is testing important?

At first it might seem like you don't really need tests in your app. When your app is small and has limited functionality, it's easy to test it manually and determine if everything is working correctly. However, as your app grows, manual testing takes much more effort than writing automated tests. Furthermore, once you start working on professional-level apps, testing becomes critical when you have a large user base. You must account for many different types of devices running many different versions of Android. Eventually, you reach a point where automated tests can account for the majority of usage scenarios significantly faster than manual tests. When you run tests before you release new code, you can make changes to the existing code so that you avoid the release of an app with unexpected behaviors. Remember that automated tests are tests executed through software, as opposed to manual tests, which are carried out by a person who directly interacts with a device. Automated testing and manual testing play a critical role in ensuring that users of your product have a pleasant experience. However, automated tests can be more precise and they optimize your team's productivity because a person isn't required to run them and they can be executed much faster than a manual test.

A closer look at unit tests

In this codelab, you focus on unit tests. You cover instrumentation tests later on. To start, you look at the tests that are generated when you create an Android app through Android Studio. You also get some hands-on experience running the tests and gaining some familiarity with writing test code.

In a previous pathway, you went over where to find source files for tests. Unit tests are always located in the test directory:

f02b380da4e8f661.png

  1. Open the app/build.gradle file and look at the dependencies. You see some dependencies marked as testImplementation and androidTestImplementation, which correspond to unit and instrumentation tests, respectively. Of note, is:

app/build.gradle

testImplementation 'junit:junit:4.12'

The JUnit library that drives your unit tests, and lets you mark code as a test so that it can be compiled and run in such a way that it can test app code.

  1. In the test directory, open the ExampleUnitTest.kt file.

You should see a sample unit test that looks like this:

ExampleUnitTest.kt

class ExampleUnitTest {
   @Test
   fun addition_isCorrect() {
       assertEquals(4, 2 + 2)
   }
}

While you added some code to the Dice Roller app, you likely didn't write any tests. As such, there's nothing but some generic code that's created automatically by Android Studio. This is an arbitrary test that serves as a placeholder for more relevant tests that the developer is expected to write. Currently, this block of code only tests that 2 + 2 = 4. Of course, it's always true. Take a closer look at what's happening:

  • Test functions must first be annotated with the @ Test annotation imported from the org.junit.test library. You can think of annotation as metadata tags for a piece of code that can change the way the code is compiled. In this case, the @Test annotation lets the compiler know that the following method is a test, which lets it run as such.

Following the annotation, you have a function declaration, in this case the addition_isCorrect()function. Inside the function, the assertEquals() function asserts that an expected value should equal an actual value obtained through business logic. Assertion methods are the end goal of a unit test. Ultimately, you want to assert that a result obtained from your code is in a particular state. If the state of the result matches the expected state, the test passes. If the state of the result doesn't match the expected state, the test fails. In this case, the code is comparing two values, so the assertEquals() method takes two parameters—an expected value and an actual value. True to its name, the expected value is what you expect a particular result to be,in this case 4. The actual value represents the result of an actual piece of code. Generally, this would test a piece of code from the app itself. In this case, it's only an arbitrary piece of code, for example, 2 + 2. Without further ado, run this test to see what happens.

There are many ways to run tests in Android Studio, which you dive into later. Now, you keep it simple.

  1. Next to the addition_isCorrect method declaration, click the arrows and then select Run ‘ExampleUnitTest.addition_isCorrect'.

78c943e851a33644.png

This is what is referred to as a positive test. In other words, the assertion is in the affirmative. 2 + 2 is equal to 4. Alternatively, we could write a negative test which makes an assertion in the negative. For example: 2 + 2 is not equal to 5.

In the Run pane, you should see something like this screenshot:

190df0c8ff787233.png

There are various indications that the test succeeded, namely green checks and the number of tests that passed.

aa7d361d8e4826ef.png

  1. Modify the test to see what a failure looks like. Change 2 + 2 to 2 + 3, then execute the test again. Keep in mind that you're only experimenting with the generated code to gain experience with how tests work. These changes don't hold any relevance to the Dice Roller functionality.

ExampleUnitTest.kt

class ExampleUnitTest {
   @Test
   fun addition_isCorrect() {
       assertEquals(4, 2 + 3)
   }
}

After you run the rest, you should see something like this screenshot:

751ac8089cf4c47c.png

The red text indicates a test failure. In the menu of test results, clicking on the items in the provides an error message that indicates why the test failed.

163708373e651ecc.png

In this case, the message indicates that the assertion failed because it expected a result of 4, but the actual value was 5. This makes sense because you changed the actual value to be 2 + 3, but you left the expected value as 4. You can also see the line at which the test failed. In this case, it's line 15, denoted as ExampleUnitTest.kt:15.

  1. For the sake of thoroughness, change the expected value from 4 to 5 and run the test again. The test should now pass as the expected value matches the actual result of the code in question.

3. Write your first unit test

Now that you've gained some comfortability with unit tests, you can write your own unit test that's more relevant to the Dice Roller app.

As you've already noticed, the Dice Roller app's primary functionality is based on a random-number generator. Unfortunately, random-number generators are notoriously difficult to test because you can't be sure of the outcome of a randomly generated number. The goal of this test is to ensure that when you roll the dice, or call the roll method on the dice class, you get an appropriate number back. The test you write simply tests that the output of the random-number generator is a number within the range you specified to the generator.

  1. In the ExampleUnitTest.kt file, delete the generated test method and import statements. Your file should now look like this:

c06e8b402f293b5e.png

  1. Create a generates_number() function:

ExampleUnitTest.kt

fun generates_number() {
}
  1. Annotate the generates_number() method with the @Test annotation. Notice that when you try to call @Test, the text is red. This is because it can't find the declaration of this annotation so you need to import it. You can do this automatically when you press Control+Enter (or Options+Return on Mac).

If you click the line of code, you should see a prompt to import:

bbe5791b9565588c.png

Alternatively, you can also copy and paste the import org.junit.Test file after the package name, but before the class declaration. The code should now look like this:

9a94c2bdf84adb61.png

  1. Create an instance of the Dice object.

ExampleUnitTest.kt

@Test
fun generates_number() {
   val dice = Dice(6)
}
  1. Next, call the roll() method on this instance and store the returned value.

ExampleUnitTest.kt

@Test
fun generates_number() {
   val dice = Dice(6)
   val rollResult = dice.roll()
}
  1. Finally, make an actual assertion. In other words, you need to assert that the method returned a value that's within the number of sides that you passed in. So in this case, the value needs to be greater than 0 and less than 7. To accomplish this, use the assertTrue() method. Notice that when you try to call the assertTrue()method, the text is red at first. This is because it can't find the declaration of this method so you need to import it, similar to what you encountered with the annotation.

10eea07fc21bf998.png

You can automatically import it as previously discussed. Notice, however, that this time you have multiple options to choose from. In this case, it should be the option from the org.junit.Assert package:

5dbfba2ba0e37ac9.png

Alternatively, you can paste this code after the import statement for the test annotation:

ExampleUnitTest.kt

import org.junit.Assert.assertTrue

Your code now looks like this:

347f792f455ae6b5.png

If you put your cursor between the parentheses and press Control+P (or Command+P on Mac), you see a tooltip that shows what parameters the method takes:

865cf0ac47738e08.png

The assertTrue()method takes two parameters: a String and a Boolean. If the assertion fails, the string is a message that displays in the console. The boolean is a conditional statement. Set the message to:

ExampleUnitTest.kt

"The value of rollResult was not between 1 and 6"

As mentioned earlier, testing random numbers is a challenge because the value of the number can't be predicted due to the nature of its randomness. All that can be done is to ensure the value is within a particular range. Set the condition parameter to:

ExampleUnitTest.kt

rollResult in 1..6

The code should look like this:

ExampleUnitTest.kt

@Test
fun generates_number() {
   val dice = Dice(6)
   val rollResult = dice.roll()
   assertTrue("The value of rollResult was not between 1 and 6", rollResult in 1..6)
}
  1. Click the arrows next to the function and then select Run ‘ExampleUnitTest.generates_number()'.

If your code looks like the previous code snippet, your test should pass!

  1. Optional: For extra practice, modify the dice to be 4- or 5-sided without changing the assertion to see the test fail.

4. Congratulations

You learned:

  • The importance of testing.
  • What a unit test looks like.
  • How to run a unit test.
  • Some common testing syntax.
  • How to write a unit test.

Learn more