Writing Instrumented Tests
Instrumented tests, to some level, work the same as unit tests:
- We use JUnit4, including annotations like
@Test
- We can apply rules, using the same
@Rule
annotation - We can use mocks, if desired
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 ToDoTests
— RosterListFragmentTest
— 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:
- Get our
Context
- Use that to get an in-memory
ToDoDatabase
implementation - Wrap that in a
ToDoRepository
(held in arepo
property) - Call the
loadKoinModules()
top-level function tooverride
our normalToDoRepository
instance with the one that we just created - Put three test objects into that in-memory database via the repository:
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:
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.