Mocking Android resources with Mockito and Kotlin
I bumped into an issue that was a little harder than I expected to solve, so this is the documentation.
Requirement: Load a JSON file from the res/raw
resource directory.
Actually, that wasn’t the problem. The problem was how do you test that functionality?
The library
I have a basic configuration library that is constructed like this:
class Configuration internal constructor(jsonContext: String): Map<String, Any> {
internal constructor(stream: InputStream)
: this(stream.bufferedReader(Charsets.UTF_8)).readText()
constructor(context: Context, @RawRes resourceId: Int)
: this(context.resources.openRawResource(resourceId))
constructor(context: Context, resourcesName: String)
: this(context, context.resources.getIdentifier(resourceName, "raw", context.packageName))
init {
val mapper = jacksonObjectMapper()
configuration = mapper.readValue(jsonContent)
}
// Rest of my class here
}
All the secondary constructors call one another in a cascade. If I provide a resource name, it looks it up, calling the constructor above it (which has a resource ID) that opens the resource before calling the constructor above it (which takes a stream) that loads the JSON, which calls the primary constructor, which parses the JSON.
I need to mock two methods within the context:
context.resources.openRawResource()
context.resources.getIdentifier()
With my new best friend, Mockito, this should be easy.
Add Mockito to the build.gradle file
I use both mockito
and mockito-kotlin
in my testing. You will see why in a moment. Here are my dependencies:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.core:core-ktx:1.1.0'
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.1"
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.23.0'
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
testImplementation "androidx.test:core:1.2.0"
}
Create a BaseTest class
I tend to have a bunch of utility functions for loading JSON files and mocking. They are used throughout the tests, so I put them in an abstract BaseTest
class:
abstract class BaseTest {
fun openJsonFile(filename: String): InputStream
= javaClass.classLoader!!.getResource(filename).openStream()
/**
* Produces a context that supplies a resource for testing.
*/
fun getTestContext(id: Int, resource: String): Context {
val resources = mock<Resources> {
on { openRawResource(eq(id)) } doReturn(openJsonFile("${resource}.json"))
on { getIdentifier(eq(resource), eq("raw"), any()) } doReturn(id)
}
val context = mock<Context> {
on { getResources() } doReturn(resources)
}
return context
}
}
The getTestContext()
method returns a mocked Android context that will allow you to interact with a resource. This allows me to test different files at different times.
I can then write my test as follows:
class AzureConfigurationTest: BaseTest() {
@Test
fun `can load a configuration from a resource name`() {
val context = getTestContext(1001, "flat")
val actual = Configuration(context, "flat")
assertNotNull(actual)
}
}
The test is good, but what went wrong?
- The
openRawResource
method always said the stream was closed when attempting to read from it. - The
getIdentifier
method always returned 0, indicating an error.
Fix the mock
The problem, in both cases, is in how I constructed the mock. There are two mechanisms for returning data through the mock. doReturn
is for static data. doAnswer
is for dynamic data. A stream is always dynamic data. I can adjust the openRawResource
call as follows:
val resources = mock<Resources> {
on { openRawResource(eq(id)) } doAnswer {
val file = "${resource}.json"
openJsonFile(file)
}
//...
}
In this interim state, I changed eq(id)
to any()
to see if it worked. This was actually how I discovered that the getIdentifier()
call was returning 0.
The second problem was a little harder to track down. I adjusted the mock again to the following:
val resources = mock<Resources> {
on { openRawResource(eq(id)) } doAnswer {
val file = "${resource}.json"
openJsonFile(file)
}
on { getIdentifier(eq(resource), eq("raw"), any()) } doAnswer {
id
}
}
Now, set a breakpoint on the second doAnswer
block. It never gets executed. Looking at the code and thinking through the process, I thought - “hmmm - I’m not mocking context.packageName
. What happens when the packageName is null?”
Short version: any()
does not match null. So I have to also mock the packageName:
fun getTestContext(id: Int, resource: String): Context {
val resources = mock<Resources> {
on { openRawResource(eq(id)) } doAnswer {
val file = "${resource}.json"
openJsonFile(file)
}
on { getIdentifier(eq(resource), eq("raw"), any()) } doReturn(id)
}
val context = mock<Context> {
on { getResources() } doReturn(resources)
on { packageName } doReturn(javaClass.canonicalName)
}
return context
}
With this version, I learned two valuable lessons:
- Use
doAnswer
during the writing process - it allows you to set breakpoints. - Make sure you mock everything you are going to use. Everything else will return null.
Yes, I looked at Robolectric for this. While I like the idea of Robolectric for applications, it doesn’t provide enough control over how the test files are injected for me to use it. Mockito was just easier.
Leave a comment