Test-Driven Development Tutorial for Android: Getting Started

Learn the basics of test-driven development, or TDD, and discover how to use TDD effectively when developing your Android apps!

Version

  • Kotlin 1.2, Android 4.4, Android Studio 3

Testing is an important part of software development. By including tests with your code, you can ensure that your code additions work and that later changes don’t break them. They can give you the peace of mind to develop quickly and catch bugs before they’re released.

Test-Driven Development, also known as TDD, is one way of ensuring to include tests with any new code. When following this process, you write the tests for the thing you are adding before you write the code to implement it. Using TDD when developing an Android app is what you will learn in this tutorial, and by the end, you’ll understand:

  • The features and uses of TDD.
  • Why TDD is useful.
  • The steps for practicing TDD.
  • How to use TDD in your own projects.
  • How to write tests for ViewModel instances that use LiveData, both part of the Architecture Components from Google.
Note: This tutorial assumes that you have basic knowledge of Kotlin and Android. If you’re new to Android, check out our Android tutorials.
If you know Android, but are unfamiliar with Kotlin, take a look at Kotlin For Android: An Introduction.

You should also have basic knowledge of ViewModel and LiveData (part of the Android Architecture Components library from Google), so have a look at Android Architecture Components: Getting Started.

Knowledge of mocking and testing on Android is helpful, but not necessary. You can learn about mocking with Mockito in Android Unit Testing with Mockito.

What Is TDD?

TDD is a software-development process. You can apply TDD wherever you practice software development, whether it’s iOS, Android, back-end, front-end, embedded, etc. You may have also heard it described as Red-Green-Refactor. It’s a process in which you write the tests that specify the code you’re going to write before you start writing any of the code.

Why Is TDD Important?

There are a number of motives for using TDD, and they all have a lasting impact:

  • Faster development time: When you have well-written tests, they provide an excellent description of what your code should do. From the start, you have the end goal in mind. Writing these specifications as tests can help keep the result from drifting away from the initial idea.
  • Automatic, up-to-date documentation: When you’re coming into a piece of code, you can look at the tests to help you understand what the code does. Because these are tests rather than a static document, you can be sure this form of documentation is likely up-to-date!
  • More maintainable code: When practicing TDD, it encourages you to pay attention to the structure of your code. You will want to architect your app in a way that is easier to test, which is generally cleaner and easier to maintain and read. For example, decoupled classes are easier to set up test classes for, encouraging you to structure your classes this way.

    Refactoring is also built into this development process. By having this refactoring step built in, your code starts off squeaky clean!

  • Greater confidence in your code: Tests help you to ensure that your code works the way it should. Because of this, you can have greater confidence that what you wrote is “complete.” In addition, with any changes that you make in the future, you can know that you didn’t break that functionality as long as the tests you wrote with the code are passing.
  • Higher test coverage: This one is fairly obvious. If you’re writing tests along side the code you’re writing, you’re going to have more test coverage over the code! This is important to many organizations.

Pile of treasure

The Five Steps of TDD

Through the TDD process, you’ll write a number of tests. You usually want a test for the happy path and at least one sad path. If there is a method with a lot of branching, it’s ideal to have a test for each of the branches.

You accomplish TDD by following five steps, which you’ll walk through over the course of this tutorial:

  1. Add a test: Anytime you start a new feature, fix or refactor, you write a test for it. This test will specify how this change or addition should behave. You only write the test at this step and just enough code to make it compile.
  2. Run it and watch it fail: Here, you run the tests. If you did step one correctly, these tests should fail.
  3. Write the code to make the test pass: This is when you write your feature, fix or refactor. It will follow the specifications you laid down in the tests.
  4. Run the tests and see them pass: Now, you get to run the tests again! At this point, they should pass. If they don’t, go back to step three until all your tests are green!
  5. Do any refactoring: Now that you have a test that makes sure your implementation matches the specifications, you can adjust and refactor the implementation that you have to ensure that it’s clean and structured the way you want without any worries that you’ll break what you just wrote.

Getting Started

Sometimes, it’s important to be proud of the little things. In this tutorial, you’ll work on an app (and tests!) to keep track of Small Victories. This app has a place to put the name of the small victory you want to track, such as choosing a healthy snack. It will also include a counter, a way to increment your victories and an option to clear the victories.

Download the Starter App

Start by downloading the sample project using the button at the top or bottom of this tutorial. If you already have Android Studio 3.1.3 or later open, click File ▸ Import Project and select the top-level project folder for the starter project that you just downloaded. If not, start Android Studio and select Open an existing Android Studio project from the Welcome screen, again choosing the top-level project folder for the starter project that you just downloaded.

Once you have the project opened and synced, build and run the app. It won’t let you do much other than set your victory title yet (by tapping the text view near the top of the screen). The rest is for you to implement.

App screen with title "Your Victory" at top, star in center, and FAB at bottom right

There are a couple of files that you should locate and become familiar with as they are the ones you’ll work with in this tutorial.

  • MainActivity.kt: This is where you’ll put any changes that affect the view.
  • MainActivityTest.kt: Here is where the tests go for MainActivity.
  • VictoryViewModel.kt: The ViewModel will contain the logic you’ll work with.
  • VictoryViewModelTest.kt: Likewise, this is where you’ll test VictoryViewModel.

You’ll also interact with the VictoryRepository interface, but you won’t need to change anything there. Similarly, you will use VictoryUiModel to represent state, but it won’t require any changes.

Instrumentation and Unit Tests

Instrumentation tests are for the parts of your code that are dependent on the Android framework but that do not require the UI. These need an emulator or physical device to run because of this dependency. You are using the architecture component ViewModel, which requires mocking the MainLooper to test, so you will use an instrumentation test for this. These tests go in a app/src/androidTest/ directory with the same package structure as your project.

A unit test, in contrast with an instrumentation test, focuses on the small building blocks of your code. It’s generally concerned with one class at a time, testing one function at a time. Unit tests typically run the fastest out of the different kinds of tests, because they are small and independent of the Android framework and so do not need to run on a device or emulator. JUnit is usually used to run these tests.

In order to ensure that you’re purely testing just the class of interest, you mock or stub dependencies when writing unit tests. Because unit tests are independent of the Android framework, they generally go in the app/src/test/ directory with the same package structure as your project. You can learn all about unit tests on Android here.

You will write your instrumentations tests in this tutorial much like a unit test, with the exception of the ViewModel dependency on the Android framework.

Android reading

Writing a Failing Instrumentation Test

The first task you’re going to complete is implementing the increment victory count. When you tap the Floating Action Button in the bottom corner, the behavior you want is that it will increment a count in the star in the center of the screen. If you take a look at the VictoryViewModel, you might get a hint of how you will implement it. There is already an incrementVictoryCount() method that you will fill in. But first — tests!

Open up the VictoryViewModelTest and take a look:

// 1
@Rule
@JvmField
var instantTaskExecutorRule = InstantTaskExecutorRule()

// 2
private val viewStateObserver: Observer<VictoryUiModel> = mock()
private val mockVictoryRepository: VictoryRepository = mock()

// 3
private val viewModel = VictoryViewModel()

@Before
fun setUpTaskDetailViewModel() {
  // 4
  viewModel.viewState.observeForever(viewStateObserver)
  viewModel.repository = mockVictoryRepository
}
  1. For testing purposes, you need an InstantTaskExecutorRule to avoid the check of setting values to LiveData objects on threads that are not the Android main thread. The @JvmField annotation is used to expose the rule as a field. This is required by the Android JUnit test runner.
  2. Here is the initialization of objects being mocked for testing the view model.
  3. Here you instantiate the view model to be tested.
  4. In the test setup method, which is tagged with a @Before annotation, you use mocks to set properties on the VictoryViewModel, setting up an observer for the LiveData and the repository.

At the bottom of the test class, you can see helper stub methods. You don’t have to worry about how to stub in this tutorial, but you can learn about stubs here.

In the middle of the test class, you’ll see existing tests for the interactions with the title. You’ll write your new tests in a similar way.

Note: When adding tests to your own code, Android Studio has a handy, built-in shortcut for getting started. By using the keyboard shortcut Cmd+Shift+T (on Mac) or Alt+Shift+T (on PC) while in the class to be tested, Android Studio will give you an option to create an empty test class for you in the test/ or androidTest/ directory; it will have the same package structure as where it is in your main project. You can also use it to jump to an existing test. Dropdown containing options to navigate to test or create a new test

There are a couple things you want to make sure happen when the count increments, and you will want a test for each one.

  • Get the old count from the VictoryRepository.
  • Increment that count, and save it to the VictoryRepository.
  • Notify the view of a new count to display using a VictoryUiModel.

When writing tests, you only want to test one thing in each test method. This maps to one verify() per test.

Add this test to VictoryViewModelTest to test for getting the previous count:

@Test
fun incrementVictoryCountCallsRepository() {
   stubVictoryRepositoryGetVictoryCount(5) // Arrange
   viewModel.incrementVictoryCount() // Act
   verify(mockVictoryRepository).getVictoryCount() // Assert
}

In the new test, you’re doing the following:

  • First, you’re stubbing the VictoryRepository to return a count of 5. Remember that you are only testing the VictoryViewModel here, and you don’t care about the repository implementation. This is using Mockito to do the work.
  • Then, you calling the method under test, incrementVictoryCount().
  • Finally, there’s the verification. You verify that the method getVictoryCount() is called on the repository — here called mockVictoryRepository. This is also using Mockito.

The code in this test follows the TDD ArrangeActAssert pattern for testing. You first arrange the setup for testing, you perform the action that you’re testing, and you then assert that the expected result has occurred.

You will follow the same pattern for writing tests for the other two conditions. Add the test for saving the incremented count to the repository.

@Test
fun incrementVictoryCountUpdatesCount() {
   val previousCount = 5
   stubVictoryRepositoryGetVictoryCount(previousCount)
   viewModel.incrementVictoryCount()
   verify(mockVictoryRepository).setVictoryCount(previousCount + 1)
}

In this code, you stub getting the previous count that you saw before calls to incrementVictoryCount(), and you then verify that setVictoryCount() was called on the repository with the correct, incremented value.

Now, add the test for the final condition on incrementing the count: notifying the view to update. This is done by setting the value on the LiveData that the view is observing:

@Test
fun incrementVictoryCountReturnsUpdatedCount() {
   val previousCount = 5
   stubVictoryRepositoryGetVictoryCount(previousCount)
   viewModel.incrementVictoryCount()
   verify(viewStateObserver)
       .onChanged(VictoryUiModel.CountUpdated(previousCount + 1))
}

Because you’re testing the value set on the LiveData here, the verification looks different. The verification is on the mock viewStateObserver, which is observing the LiveData. You verify that it has changed, and that it’s the value you’re expecting: a VictoryUiModel.CountUpdated with the correct incremented count.

Running the Tests

Now that you have your tests written, you can run them and see them fail, following step two of the TDD process. You can run them right in Android Studio a couple of different ways. In the test class file itself, there should be icons in the gutter that you can click to run the full test class or a single test method.

Green triangle run test button in code gutter

You can also right-click on a test class name, file or directory in the Android Studio Project pane to run everything in that class, file or directory.

Drop down with "Run VictoryViewModelTest..." highlighted

Run all the tests in VictoryViewModelTest and see the new tests you’ve added fail. You’ll be prompted to pick an emulator or device to run the tests on.

Test results with newly written increment tests failing

Making the Tests Pass

In the failing tests, you have specified what the code should do, so now you can go on to step three to write the code to implement the feature and make the test pass. Locate the incrementVictoryCount() method in VictoryViewModel. Add the following code to that method:

val newCount = repository.getVictoryCount() + 1
repository.setVictoryCount(newCount)
viewState.value = VictoryUiModel.CountUpdated(newCount)

These three lines correspond to the three tests you wrote: one to get the count from the repository, one to save the new count, and one to notify the view.

Running the Tests and Watching Them Pass

You can run the same tests by clicking the green triangle next to the Run Configuration dropdown.

Dropdown with text "VictoryViewModelTest" next to green triangle

When you run the tests now, they should be green! You now know that your implementation matches the specifications.

Test results with newly written increment tests passing

You can experiment with commenting out or removing one line at a time to see what tests pass and what test fails as a result. Just make sure that you put all the code back when you’re done.

Refactoring

Now that you have written the tests and gotten them passing, this is where you would do any refactoring to make your code as nice as possible while keeping your tests green. This tutorial doesn’t have a specific refactor for you for this method, as it is a simple example, but if you are feeling creative you’re welcome to refactor this method the way you want as long as your tests are still passing!

Writing a UI Test

UI tests test what the user sees on the screen. They are dependent on the Android framework and need to run on a device or emulator. Like instrumentation tests, they also go in the androidTest/ directory.

UI Tests are the slowest to run out of all three of these categories of tests, so you want to be selective about what you test with these. You want to test as much of your logic as you can in unit and instrumentation tests, as they run faster.

On Android, UI tests usually use the Espresso library to interface with and test the view. Mockito is also sometimes used here.

Something to note when you’re adding test library dependencies to your project: In your app module build.gradle file, you specify whether the dependency is for an Android test or unit test. If you take a look at the app/build.gradle file in this project, you’ll see that some dependencies such as JUnit use testImplementation, and others, such as Espresso, use androidTestImplementation. This matches up with whether the test file is in the test/ or androidTest/ folders.

testImplementation 'junit:junit:4.12'
// ...
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

Writing a Failing UI Test

You have the logic set up for incrementing the victory count. It’s time to hook it up so that there is feedback for the user on the view so they can see the incremented count. Just as before, you start by writing a test.

Take a look at MainActivityTest:

@LargeTest
class MainActivityTest {

  @Rule
  @JvmField
  var rule = ActivityTestRule(MainActivity::class.java)
  • @LargeTest means that the tests in this file could have more than 2 seconds of execution time, and is used for tests that make use of all platform resources, including external communications.
  • ActivityTestRule is used to launch MainActivity before any test is executed.
  • As for the instrumentation tests from before, you’ll also see some existing tests for the victory title in MainActivityTest.

    Note: When getting started with Espresso UI tests, you can use the Espresso Test Recorder to manage your test setup. You can also use this to write tests, but they tend to generate an excessive amount of test code and don’t fit the TDD approach very well.

    Add a test to MainActivityTest to verify that the count updates on the screen when someone clicks the button:

@Test
fun incrementingVictoryCountUpdatesCountView() {
  // 1
  val previousCountString = rule.activity
      .findViewById<TextView>(R.id.textVictoryCount).text.toString()
  val previousCount =
      if (previousCountString.isBlank()) {
        0
      } else {
        previousCountString.toInt()
      }

  // 2
  onView(withId(R.id.fab))
      .perform(click())

  // 3
  onView(allOf(withId(R.id.textVictoryCount),
      withText((previousCount + 1).toString())))
      .check(matches(isDisplayed()))
}

You’ll likely need to use Alt+Enter (on PC) or Option+Return (on Mac) to add the imports for some of these things.

The new UI test follows the same Arrange-Act-Assert pattern as before. There’s some set up, the action you’re testing being performed, and the verification:

  1. Get the previous count from the view. Because you’re not stubbing the repository here, you’re getting the previous count directly from the view. You start by using the activity to find the TextView to get the string value from. Then, the value is converted to an integer.
  2. Click on the Increment Victories (FAB) button. By using Espresso, you can look up the view by the ID, and then perform a click on it.
  3. Verify that the TextView displaying the count updates. With Espresso, you can look for a view by multiple attributes. Here, you’re looking for a view with the ID of the counter view and the text of the count you are expecting. Then you use a match to see that it exists and is displayed.
Note: Instead of using findViewById, you could directly use the name of the view, thanks to the Kotlin Android Extensions. So, in this example, you could use:
val previousCountString = rule.activity.textVictoryCount.text.toString()
You will need to import kotlinx.android.synthetic.main.content_main.* if you try this.

Running the Tests

Run the MainActivityTest the same way as you ran the VictoryViewModelTest. The new test that you just wrote should fail.

Test results with newly written UI increment tests failing

Making the Tests Pass

Since you put all the logic into VictoryViewModel, there’s not much to do in MainActivity to get this test passing. You just have to update the view!

The code is already set up in MainActivity to observe the changes to the LiveData in the VictoryViewModel and call render() with the changes. Look for the render() method in the MainActivity.

In the VictoryUiModel.CountUpdated branch of the when statement, add the following code:

textVictoryCount.text = uiModel.count.toString()

Here, with a VictoryUiModel.CountUpdated instance named uiModel, you update the text of the victory count view with the new count.

Running the Tests

Run the tests again. This time you should see them pass!

Test results with newly written UI increment tests passing

Build and run the app. Now, when you click the increment button, you will see the count update as well.

Gif of pressing the button causing the count to increment

Just like for the earlier instrumentation test, this is the point at which you would revisit the new code you’ve added and make any refactoring changes.

Writing a Test to Fix a Bug

TDD is not only used for adding new features, but also for fixing bugs and code refactoring. Before you go to fix a bug, you should write a test for what the behavior should be after you’ve fixed it. Then, you can be sure that you fixed it and that it won’t come back again in the future.

If you’ve been playing around with the app or looking closely at the code, you may have spotted a bug. Whenever you change the title, it changes the count to zero, and it doesn’t correct itself until the next time the count increments.

Gif of changing title resetting count

This is the bug you’re going to fix here. Start by writing a test in the MainActivityTest to make sure the count is not updated when the title changes.

@Test
fun editingTitleDoesntChangeCount() {
  // 1
  onView(withId(R.id.fab))
     .perform(click())
  // 2
  onView(withId(R.id.textVictoryTitle))
     .perform(click())
  val newTitle = "Made the bed"
  onView(instanceOf(EditText::class.java))
     .perform(clearText())
     .perform(typeText(newTitle))
  onView(withText(R.string.dialog_ok))
     .perform(click())

  // 3
  onView(allOf(withId(R.id.textVictoryCount), withText("0")))
     .check(doesNotExist())
}

Here, you are:

  1. Performing a click to the increment victories fab to make sure the count is non-zero to start.
  2. Updating the title.
  3. Making sure that the count did not reset to zero.

Running the Failing Test

Now run the test and watch it fail:

Test results with newly written bug test failing

Making the Test Pass

Take a look at the setVictoryTitle() method in VictoryViewModel. There’s one extra line that is the cause of this bug. Maybe a previous developer who was unsure about what the behavior should be left it there. He or she should have written a test!

Remove this line from the setVictoryTitle() method:

viewState.setValue(VictoryUiModel.CountUpdated(0))

Run the test again, and this time it should be green.

Test results with newly written bug test passing

Run the app again. You’ll see that you fixed the bug. Yay! You also know this bug won’t show up again in the future when you forget, er, some other developer forgets what this method should do.

Writing a Test Before Refactoring

Many times in your career as an Android developer, you’ll inherit code that does not have existing tests but that needs some refactoring.

While not TDD in the “Red-Green-Refactor” sense, it’s still great to write tests before you refactor existing code, especially if that bit of code does not have tests yet. You’re going to refactor the implementation of the initialize() method in the VictoryViewModel.

Take a look at the current method:

fun initialize() {
  viewState.value = VictoryUiModel.TitleUpdated(repository.getVictoryTitle())
  viewState.value = VictoryUiModel.CountUpdated(repository.getVictoryCount())
}

You know that it should pass both the victory title and the victory count along to the viewState LiveData instance. Right now, it is making two calls to the repository to do this, but there is also a repository method, getVictoryTitleAndCount(), that will return both.

For the sake of this exercise, you will only write the tests for verifying that the view updates. You can add tests for the repository call on your own, if you like.

Add these two tests to the VictoryViewModelTest; they are similar to the other tests you’ve added:

@Test
fun initializeReturnsTitle() {
   val title = "New title"
   val count = 5
   stubVictoryRepositoryGetVictoryTitleAndCount(Pair(title, count))

   viewModel.initialize()

   verify(viewStateObserver).onChanged(VictoryUiModel.TitleUpdated(title))
}

@Test
fun initializeReturnsCount() {
   val title = "New title"
   val count = 5
   stubVictoryRepositoryGetVictoryTitleAndCount(Pair(title, count))

   viewModel.initialize()

   verify(viewStateObserver).onChanged(VictoryUiModel.CountUpdated(count))
}

Running the Tests

When you run these tests, make sure they pass. This time, you’re verifying existing behavior to make sure that it doesn’t change with your refactor.

Passing initialize test

Refactoring Your Code

Since there is a repository method to get both the count and title, you should use that one instead. Replace the contents of the initialize() method in VictoryViewModel with the following code:

val (title, count) = repository.getVictoryTitleAndCount()
viewState.value = VictoryUiModel.TitleUpdated(title)
viewState.value = VictoryUiModel.CountUpdated(count)

Here, you’re using the repository method to get both the title and the count, then setting the values returned for the view to display.

Run the tests in VictoryViewModelTest and see that they all still pass.

Passing initialize test

Where to Go From Here

You can download the finished version of this project using the Download materials button at the top or bottom of this tutorial.

You now know what it means to practice TDD in Android app development, and you can start using TDD on your own. That’s no small victory, but a pretty large one!

Through including the tests you write with TDD from the start, you can have more confidence in your code throughout the lifetime of your app, especially when you add new features and release new versions of the app.

Resources

As you continue to practice the TDD process, here are some more resources to help you along the way:

Challenge

Why not get started practicing TDD on your own right away? As a challenge, try implementing the “Reset” functionality found in the overflow menu. You can find the completed challenge in the same download from the Download materials button found at the top or bottom of this tutorial. Remember to write your tests first and run them first to see them fail (even if you’re feeling lazy)!

You’ve learned a lot in this tutorial, and we hope you enjoyed it! If you have any questions or comments, feel free to join the discussion forum below. :]

Contributors

Comments