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

15. Refactoring Your Tests
Written by Lance Gleason

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Sometimes you need to slow down to move fast. In development, that means taking the time to write and refactor your tests so that you can go fast with your testing. Right now your app is still fairly small, but the shelters have big plans for it. There are lots of homeless companions and pairless developers that need to be matched up! In the last chapter you started with end-to-end UI tests, added some missing coverage, and then refactored your code to made it easier to go fast.

End-to-end tests usually run in a simulator or on a device. Because of that, they take longer to build, deploy, and run. In Chapter 4, “The Testing Pyramid,” you learned about how you should aim to have a pyramid of tests, with your unit tests being the most numerous, followed by your integration tests, and finally your end-to-end tests. Right now you have an inverted pyramid where all of your tests are end-to-end.

As your app gets larger, this will slow down your development velocity because a number of things happen, including:

  • Your Espresso tests will take longer and longer for the test suite to run.
  • Tests that exercise one part of the app will often be exercising other parts of the app as well. A change to these other parts can (and will) break many tests that should not be related to what you are testing.

In this chapter you’re going to break down your tests into integration and unit-level. Along the way you will learn some tricks for mocking things out, breaking things down, and even sharing tests between Espresso and Robolectric. A lot of people are counting on you, so let’s get started!

Note: In a normal development setting, it may be considered premature optimization to refactor an app the size of your Coding Companion Finder until it gets larger. That is a trade-off we needed to make with this book. That said, there is an art to knowing when to break things down. When you are new to TDD, it is easy to slip into a rut of not testing enough and not breaking down your tests soon enough. This is because testing is hard and it is easy to say it is not worth the effort.

Until you get some experience with TDD, it is better to err on the side of over-testing and over-optimization. As you get more familiar with the tools and techniques you will be in a better place to make that determination. There will always be gray areas that experienced TDDers will disagree on.

Source sets, UTP and sharedTest

With androidx.test, Robolectric 4.0 and the Unified Test Platform (UTP), which can be found here (https://www.youtube.com/watch?v=juEkViDyzF8), you have the ability to write tests in Espresso and run them in either Robolectric on the JVM or in an emulator/real device. One common use case is to run integration and some end to end tests using the faster Robolectric while working on your local machine. Then running the same tests using slower, but closer to real life, Espresso during less frequent Continuous Integration cycles to find potential issues on specific versions of Android.

Up to this point with your refactoring, you have been focusing on running your tests in Espresso and putting them in androidTest. This is how an Android project is configured out of the box. If you want to run the same test in Robolectric you would need to move that test to the test source set or create a new test.

This limitation negates that benefit of being able to run the same test in Espresso and Robolectric (other than the shared syntax). This is a shortcoming with the current default Android project setup. Luckily, there is a way to get around this by using a shared source set.

To get started, open the starter project for this chapter or your final project from the last one. Go to the app ‣ src directory. You will see three directories there – androidTest, main and test. Add a sharedTest directory, and copy all of the contents of androidTest to your new sharedTest directory, then delete the contents of androidTest ‣ assets and androidTest ‣ java ‣ com ‣ raywenderlich ‣ codingcompanionfinder. Note: you need to leave the directories in androidTest for Android Studio to be able to know that it has integration tests, even if all of them are currently in your sharedTest directory.

Next, open your app level build.gradle and add the following under your android section:

android {
  sourceSets {
    String sharedTestDir = 'src/sharedTest/java'
    String sharedResources = 'src/sharedTest/assets'
    test {
      java.srcDir sharedTestDir
      resources.srcDirs += sharedResources
    }
    androidTest {
      java.srcDir sharedTestDir
      resources.srcDirs += sharedResources
    }
  }
}

This is creating a new source set that maps both your test and androidTest to your sharedTest directory. It is also nesting an Android directive under an Android directive so yours should look like this:

android {
  .
  .
  .
  android {
    sourceSets {
      .
      .
      .
    }
  }
  .
  .
  .
}

Note: This may look familiar from the sharedTest set up you did in Chapter 11, “User Interface.”

Now, in your sharedTest ‣ java ‣ com ‣ raywenderlich ‣ codingcompanionfinder package open CommonTestDataUtil.kt. In the first line of your readFile function get rid of the /assets in this line:

val inputStream = this::class.java
  .getResourceAsStream("/assets/$jsonFileName")

so that it looks like this:

val inputStream = this::class.java
  .getResourceAsStream("/assets/$jsonFileName") ?:
  this::class.java
  .getResourceAsStream("/$jsonFileName")

Run your tests in Espresso (you might need to sync Gradle first) and they will be green.

Note: If you find some of the tests are failing, check that MainActivity.accessToken is set to your token you retrieved in Chapter 13.

Now that you have your tests moved to a sharedTest source set, there are a few things you need to do in order to get them working with Robolectric.

First, open your app level build.gradle and add the following to the dependencies section:

testImplementation 'androidx.test:runner:1.3.0'
testImplementation 'androidx.test.espresso:espresso-core:3.3.0'
testImplementation "androidx.test:rules:1.3.0"
testImplementation "androidx.test.ext:junit:1.1.2"
testImplementation "androidx.navigation:navigation-testing:2.3.5"
testImplementation 'com.squareup.okhttp3:mockwebserver:3.12.0'
testImplementation "androidx.test.espresso:espresso-contrib:3.3.0"
testImplementation 'org.robolectric:robolectric:4.5.1'
testImplementation 'org.koin:koin-test:2.0.1'
androidTestImplementation "org.robolectric:annotations:4.5.1"

This is adding all of the dependencies that you had for your Espresso tests at the unit level. It is also including the Robolectric dependencies that you will need. Next, add the following to the top level android section of the same file:

testOptions {
  unitTests.includeAndroidResources = true
  unitTests.returnDefaultValues = true
}

These are telling Robolectric to include Android resources. Because Robolectric is not an actual emulator or device, many Android system calls do not actually do anything. The unitTests.returnDefaultValues makes them return a dummy default value in those instances, instead of throwing an exception.

Arctic Fox, Bumble Bee and the New Test Runners

At the time of this writing. The current, released version of Android Studio is 4.2.1. In this version, along with older versions, the test runners for both unit and Espresso driven tests are different in Android Studio than they are when running the Gradle build from the command line. That can cause tests to behave slightly different when running in a CI environment. To reduce that uncertainty, Android Studio Arctic Fox migrated the built-in unit test runner to run through Gradle. Android Studio Bumble Bee moves the Espresso driven integration tests to use the same runner.

Running Your Source set in Android Studio 4.2.1 and Older

If you are using Android Studio Arctic Fox or newer you can skip ahead to the next section. Otherwise, go to your app component drop-down at the top of your IDE and select Edit Configurations.

Arctic Fox and Later

If you are using Arctic Fox or later, under your project side tab make sure that you have it in the Android view mode.

Your Running Tests

After following the steps for your version of Android Studio, your tests will run and you should see the following.

Testing fragments in isolation

Until now your tests have been large end-to-end UI tests. That said, some of your test cases are actually testing one component that could be tested in isolation. A good example of that is your ViewCompanionFragment. This fragment is called via your SearchForCompanionFragment.

Breaking out unit tests

Up to this point your tests have had dependencies on Android. But, as we discussed in Chapter 4, “The Testing Pyramid,” you should strive to have unit tests. Ideally, you will have more unit tests than integration tests and more integration tests than end-to-end/UI tests.

Unit testing Retrofit calls

Now that you have your ViewCompanionViewModel under test, let’s do the same for your SearchForCompanionViewModel.

DRYing up your tests

Tests are code that you need to maintain, so let’s write some more tests for your SearchForCompanionViewModel and DRY (Do not repeat yourself) them up along the way. To get started, add the following test:

Challenge

Challenge: Test and edge cases

  • If you didn’t finish out your test cases for your ViewCompanionViewModel to test the other data elements, add tests following a red, green, refactor pattern.
  • The tests you did for your SearchForCompanionViewModel missed a lot of data validation and edge cases. Follow a red, green, refactor pattern and try to cover all of these cases with very focused assertions.

Key points

  • Source sets help you to run Espresso tests in either Espresso or Robolectric.
  • Not all Espresso tests will run in Robolectric, especially if you are using Idling resources.
  • As you get your legacy app under test, start to isolate tests around Fragments and other components.
  • ViewModels make it possible to move tests to a unit level.
  • Be mindful of mocking final classes.
  • It is possible to unit test Retrofit with MockWebServer.
  • Strive to practice Red, Green, Refactor.
  • As your tests get smaller, the number of assertions in each test should as well.
  • Strive towards a balanced pyramid, but balance that against the value that your tests are bringing to the project.
  • Test code is code to maintain, so don’t forget to refactor it as well.
  • Move slow to go fast.

Where to go from here?

With this refactoring you have set your project up to go fast. It will help many homeless companions, and companion-less developers get paired up. That said, there are other tips and tricks to learn in future chapters. For example, how do you deal with test data as your suite gets bigger? How do you handle permissions? Stay tuned as we cover this in later chapters!

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.
© 2026 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now