Chapters

Hide chapters

Android Test-Driven Development by Tutorials

First Edition · Android 10 · Kotlin 1.3 · AS 3.5

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section II: Testing on a New Project

Section 2: 8 chapters
Show chapters Hide chapters

Section III: TDD on Legacy Projects

Section 3: 9 chapters
Show chapters Hide chapters

18. Testing Around Other Components
Written by Lance Gleason

Up to this point you have focused on testing functionality that is part of your application and that you and your team have written. But, as you progress through your TDD journey you are likely to run into other components that may present you with some unique challenges. These generally fall into one of three categories:

  1. Testable: These are components that can be easily tested and/or verified, so no problems here.
  2. Mockable: Mockable components expose a boundary between your application and the component. Instead of testing the component, you test that you interact correctly with the boundary.
  3. Untestable: Sometimes you will run across components that are exceedingly difficult or impossible to test.

The testable

Some components you use have been designed so that they can be tested, or have end-state values that make it easy to test with them in the mix. For example, in Chapter 9, “Testing the Persistence Layer,” you learned about how to test the persistence layer in your application tests.

Some examples of Android components that are testable include:

  • Local persistence, such at MySQL and Realm-based data stores.
  • Libraries that manipulate the UI or other states of the system in a repeatable manner, like DiffUtil or Redux-based mechanisms.
  • Libraries that have testing hooks built-in.

The mockable

At times you will run across circumstances where your test needs to cross a system boundary that requires mocking. For example, in Chapter 10, “Testing the Network Layer,” the MockWebServer you are using is mocking out your okhttp calls that are made to a server. In this case, the mocking was taken care of for you. In other instances you will need to mock out system boundaries manually.

Permissions

Another common scenario is when working with a component that requires permissions to test. As a quick review, there are three types of Android permissions:

  1. Normal Permissions: These only require a declaration in the manifest to grant the user permission.
  2. Signature Permissions: The system grants these permissions at install time, but only when the app that is using it is signed by the same app that grants the permission.
  3. Dangerous Permissions: These are permissions that are accessing things such private user data, etc. In order to request these you need to have the permission specified in the app manifest and prompt the user for it at run time.

If you are testing functionality that requires normal permissions, there is nothing you will need to do other than include them in your app’s manifest. Signature permissions follow a similar pattern. Things get a little bit more involved when working with dangerous permissions. To understand this, let’s look at an example that uses the CALL_PHONE permission.

To get started import the starter Coding Companion project. Once it is imported, add your device keys you generated in Chapter 13, “High-Level Testing With Espresso,” and run the app.

Tap on Find Companion, enter a location, tap on Find followed by selecting a companion.

Now, tap on the phone number for the companion, select ALLOW to allow the permission request and then a call will begin to the shelter.

Open ViewCompanionFragment.kt and look at the setupPhoneNumberOnClick() function:

private fun setupPhonNumberOnClick(){
  fragmentViewCompanionBinding.telephone.setOnClickListener {
    // 1
    Dexter.withActivity(activity)
      .withPermission(Manifest.permission.CALL_PHONE)
      .withListener(
        object : PermissionListener {
          // 3
          override fun onPermissionGranted(
            response: PermissionGrantedResponse
          ) {/* ... */
            val intent = Intent(Intent.ACTION_CALL, Uri.parse(
              "tel:" + viewCompanionViewModel.telephone))
            startActivity(intent)
          }

          override fun onPermissionDenied(
            response: PermissionDeniedResponse
          ) {/* ... */}

          // 2
          override fun onPermissionRationaleShouldBeShown(
            permission: PermissionRequest,
            token: PermissionToken
          ) {/* ... */
            token.continuePermissionRequest()
          }

        }
      )
      .onSameThread()
      .check()
  }
}

This is doing three things:

  1. It uses the Dexter library to check for the CALL_PHONE permission.
  2. If permission has not been granted it shows a dialog asking for it.
  3. If permission is granted it starts a ACTION_CALL intent to call the number you tapped on.

Note: To learn more about the Dexter library you can visit https://github.com/Karumi/Dexter.

To test this scenario you are going to need to do two things:

  1. Grant CALL_PHONE permissions in your test.
  2. Do an assert on the intent.

To get started go to FindCompanionsInstrumentedTest.kt and add the following to the beginning of your class:

@get:Rule
val grantPermissionRule: GrantPermissionRule =
  GrantPermissionRule.grant(
    android.Manifest.permission.CALL_PHONE)

This is granting the CALL_PHONE permission for all of the tests in your class. Next add the following to your app level build.gradle in your dependencies section:

androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'

This adds in support for Espresso Intents, which allows you to assert that an intent is sent. You can learn more about Espresso Intents at https://developer.android.com/training/testing/espresso/intents.

Now add the following to the beginning of your FindCompanionsInstrumentedTest:

@get:Rule
val intentsTestRule = IntentsTestRule(MainActivity::class.java)

This adds a rule to use Espresso Intents in your test. Now add the following test:

@Test
fun verify_that_tapping_on_phone_number_dials_phone() {
  // 1
  val intent = Intent()
  val result =
    Instrumentation.ActivityResult(Activity.RESULT_OK, intent)

  intending(
    allOf(
      hasAction(Intent.ACTION_CALL)
    )
  ).respondWith(result)
  // 2
  find_and_select_kevin_in_30318()
  onView(withText("(706) 236-4537")).perform(click())
  // 3
  intended(allOf(hasAction(Intent.ACTION_CALL),
    hasData("tel:(706) 236-4537")))
}

This is doing the following:

  1. Creates a mock Intent that will be called when the ACTION_CALL intent is fired in your app.
  2. Navigates to a search result and clicks on the phone number.
  3. Asserts that the ACTION_CALL intent has been sent.

Run your test and it will now be green:

But, if you try running all of your tests you will have a failure:

If you dig into this, the root cause is that your IntentsTestRule is causing your MockWebServer to crash. Luckily your ViewCompanionTest does not depend on MockWebServer, and this test should really be there anyway.

To fix things, remove all of the changes you just made to FindCompanionsInstrumentedTest. Next, open your ViewCompanionTest and add the following to the class:

@get:Rule
val grantPermissionRule: GrantPermissionRule =
  GrantPermissionRule.grant(
    android.Manifest.permission.CALL_PHONE)

@get:Rule
val intentsTestRule = IntentsTestRule(MainActivity::class.java)

This is adding in the GrantPermissionRule and IntentsTestRule that you previously added to FindCompanionsInstrumentedTest. Next add the following test to ViewCompanionTest:

@Test
fun verify_that_tapping_on_phone_number_dials_phone() {
  // 1
  val intent = Intent()
  val result =
    Instrumentation.ActivityResult(Activity.RESULT_OK, intent)

  Intents.intending(
    CoreMatchers.allOf(
      IntentMatchers.hasAction(Intent.ACTION_CALL)
    )
  ).respondWith(result)
  // 2
  onView(withText("(706) 236-4537"))
    .perform(ViewActions.click())
  // 3
  Intents.intended(
    CoreMatchers.allOf(
      IntentMatchers.hasAction(Intent.ACTION_CALL),
      IntentMatchers.hasData("tel:(706) 236-4537")
    )
  )
}

This is adding the same test you had before without the call to navigate to this fragment since you are testing just the fragment in isolation. The tests in this file also do not depend on MockWebServer. Run all of the tests in it and they will be green.

Other mockable components

There are many classes of external components where your best testing strategy will be to mock out your interaction with them. Some good candidates for this include:

  • Glide
  • Picasso
  • Video Players
  • Intents to other applications etc.
  • Network calls
  • Interactions with sensors and hardware

The untestable

There are some components where the best TDD option is to not test it. Determining that the component is untestable can be tricky. TDD is hard. On one hand you don’t want to give up too soon on testing something. On the other hand you don’t want to spend too much time trying to test the untestable.

Some traits that may make a component untestable include:

  • It draws things on a graphical Canvas.
  • There is not a testable end state after interacting with it.
  • No test extensions or mechanisms are provided by the library author.
  • The component is implemented primarily though the NDK.
  • The library is popular, but a Google search doesn’t turn up any instructions on testing it.
  • You are making system calls to get device traits.

Let’s look at some examples to better understand the thought process that goes into this. The names have been changed to protect the innocent, but these are actual scenarios that the author encountered in projects.

Google maps Android SDK

Google Maps Android SDK https://developers.google.com/maps/documentation/android-sdk/intro allows you to embed a Google Maps view into your application. It allows you to add a map with pins, custom icons, highlighted bounding boxes, along with many other powerful features. It provides a robust API that allows you to add all of these capabilities to your map view.

If you dive in to a view generated by it, the elements on the map are not in a structure where you can verify that a component is in a specific location because it a basically a canvas view. If you do a Google search the only solutions you will find for testing this are using a UI automator to click on a specific position on a pre-determined map. That test will work fine when you have a specific screen size, but if you are supporting multiple screen sizes and DPIs, the coordinates you use for one screen size may not work for another.

Your best bet when testing with this SDK is to:

  • Test callbacks made by your map — i.e. when you click on a point on the map.
  • Test the data and items being used to create things on the map.
  • Rely on manual testing for the map functionality.

These will also work well for similiar libraries which are hard to test.

System setting API calls

Imagine you have an app that needs to get the values for some system attributes on your device such as the device’s IP address, SIM card provider and current GPS location. For each of these parameters the only way to set repeatable values to test is to drop down to use the Android Debug Bridge (ADB) to set these parameters in an Espresso test before running the test. While this can be done in unit tests, it can be a problematic solution for multiple reasons including:

  • In order to give the system time to apply the setting you will need to add Thread.sleep() calls which will cause much longer test execution times, and also might result in unreliable tests.
  • Depending on the version of Android, some settings may not be available via ADB or be very difficult to get at.
  • Some settings may only be testable on real devices, not emulators.

With enough work you might be able to create reliable, repeatable tests for these. In many cases it might be better to abstract that code into a simple library (sometimes called a shim) that is manually tested. In your unit tests you could then use Dependency Injection to mock this shim you created to test the code that depends on it.

Key points

  • Testable components have hooks or inputs and outputs that can be validated.
  • Many components are best tested by mocking your interaction with them.
  • It is possible to test dangerous permissions.
  • There are some components that are untestable.
  • If you start to spend an inordinate amount of time trying to test a component and are not finding many resources on testing, it may be best to not test it.

Where to go from here?

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.