Writing Instrumented Tests

Instrumented tests, to some level, work the same as unit tests:

However, there are some differences, such as not being able to use the backtick function naming system in Kotlin. And there will be some changes to how we set up Gradle and the sorts of tests that we wind up writing.

Configure Gradle

Just as testImplementation statements define dependencies for unit tests (test/), androidTestImplementation statements define dependencies for instrumented tests (androidTest/).

Our Gradle dependencies include four such lines:

  androidTestImplementation 'androidx.test.ext:junit:1.1.3'
  androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
  androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
  androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1'

The first one (androidx.test:runner) gives us our instrumentation testing core infrastructure. androidx.test.ext:junit and androidx.arch.core:core-testing provide some JUnit4 rules — notably, androidx.arch.core:core-testing is where utility code like that InstantTaskExecutorRule comes from. The final one is tied to Espresso for GUI testing, as we will explore later in the chapter.

Specify the Test Runner

Earlier in the module’s build.gradle file, we have a testInstrumentationRunner statement:

    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

This tells Android how to run our JUnit instrumented tests. JUnit uses “runner” classes for this role, and androidx.test.runner.AndroidJUnitRunner is one supplied by the Jetpack that knows how to run our instrumented tests inside of an Android environment.

Identify the Test Class

With unit tests, JUnit has a default test runner that is used.

With instrumented tests, not only do we need the testInstrumentationRunner declaration in Gradle, but we also need to annotate our tests with a @RunWith annotation, further clarifying what JUnit should run and how it should run the test:

@RunWith(AndroidJUnit4::class)
class RosterListFragmentTest {

Some libraries or other frameworks may tell you to use a different testInstrumentationRunner or a different @RunWith annotation to use. For standard Jetpack tests, we use androidx.test.runner.AndroidJUnitRunner and @RunWith(AndroidJUnit4::class), as shown here.

Access the Context

Frequently, in an instrumented test, we are using an instrumented test because something somewhere needs a Context.

To get a Context in the app being tested, your test code can use InstrumentationRegistry.getInstrumentation().getTargetContext() (or InstrumentationRegistry.getInstrumentation().targetContext in Kotlin):

    val context = InstrumentationRegistry.getInstrumentation().targetContext

Note that there is also a getContext() method (or context property in Kotlin). This also returns a Context, but it returns one for your androidTest/ code, not for the app being tested. Frequently, this is the wrong Context for whatever test you are trying to write.

Rewiring Koin

As noted earlier, using dependency inversion tends to make it easier for us to substitute in mocks or other replacement objects in our tests, rather than whatever code would normally be used.

In the case of the one instrumented test in ToDoTestsRosterListFragmentTest — we are using Room for a database. Room supports in-memory SQLite databases, which work just like their normal counterparts, just without the disk I/O. This executes much faster, and our data goes away once we stop referencing the database. This automatically cleans up our tests, which is very useful.

As a result, RosterListFragmentTest wants to replace the disk-based ToDoDatabase that we normally would use with an in-memory one. The ToDoDatabase has a newTestInstance() companion function that offers this:

package com.commonsware.todo.repo

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters

private const val DB_NAME = "stuff.db"

@Database(entities = [ToDoEntity::class], version = 1)
@TypeConverters(TypeTransmogrifier::class)
abstract class ToDoDatabase : RoomDatabase() {
  abstract fun todoStore(): ToDoEntity.Store

  companion object {
    fun newInstance(context: Context) =
      Room.databaseBuilder(context, ToDoDatabase::class.java, DB_NAME).build()

    fun newTestInstance(context: Context) =
      Room.inMemoryDatabaseBuilder(context, ToDoDatabase::class.java).build()
  }
}

However, our Koin module uses newInstance() and a disk-based database, not this in-memory one:

package com.commonsware.todo

import android.app.Application
import com.commonsware.todo.repo.ToDoDatabase
import com.commonsware.todo.repo.ToDoRepository
import com.commonsware.todo.ui.SingleModelMotor
import com.commonsware.todo.ui.roster.RosterMotor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.qualifier.named
import org.koin.dsl.module

class ToDoApp : Application() {
  private val koinModule = module {
    single(named("appScope")) { CoroutineScope(SupervisorJob()) }
    single { ToDoDatabase.newInstance(androidContext()) }
    single {
      ToDoRepository(
        get<ToDoDatabase>().todoStore(),
        get(named("appScope"))
      )
    }
    viewModel { RosterMotor(get()) }
    viewModel { (modelId: String) -> SingleModelMotor(get(), modelId) }
  }

  override fun onCreate() {
    super.onCreate()

    startKoin {
      androidLogger()
      androidContext(this@ToDoApp)
      modules(koinModule)
    }
  }
}

Fortunately, Koin lets us change the module contents in our test:

  @Before
  fun setUp() {
    val context = InstrumentationRegistry.getInstrumentation().targetContext
    val db = ToDoDatabase.newTestInstance(context)
    val appScope = CoroutineScope(SupervisorJob())

    repo = ToDoRepository(db.todoStore(), appScope)

    loadKoinModules(module {
      single { repo }
    })

    runBlocking { items.forEach { repo.save(it) } }
  }

Here, we:

  private lateinit var repo: ToDoRepository
  private val items = listOf(
    ToDoModel("this is a test"),
    ToDoModel("this is another test"),
    ToDoModel("this is... wait for it... yet another test")
  )

Now, when our test functions execute code that obtains a ToDoRepository from Koin, they will get the in-memory repository, not the “real” one that we would normally get.

Running the Tests

The Android Studio Java/Kotlin editor will offer the same sort of “run” gutter icons as with unit tests, so you can run individual test methods or entire test classes.

The test results will appear in the “Run” tool in Android Studio, just like with unit tests:

Instrumented Tests Results
Instrumented Tests Results

Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.