Bloco

View Original

How to test Android Apps: Other details

A lot can be said about testing. But to finish this testing guide, here are some subjects you will run into when testing Android apps.

Mocking components

We've seen how to mock dependencies in unit testing. But when testing for integration (like in instrumentation tests), you need to mock specific components of your app, globally.

You can use custom Flavours to change modules, or work with your dependency injection library to switch components for their mocked versions (an example):

Changing a Module on a Single Test Case

@HiltAndroidTest
@UninstallModules(AppModule::class)
@RunWith(AndroidJUnit4::class)
class CounterActivityTest {

    @BindValue @JvmField val flowSharedPreferences : FlowSharedPreferences = FakeSharedPreferences()
    // ...

Changing a Module on the given source set:

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [AppModule::class]
)
class AppMockModule {

    @Provides
    fun flowSharedPreferences(@ApplicationContext appContext: Context) = FakeSharedPreferences()
}

Mocking servers

If your app integrates with a server API, you don't want your tests to hit the server every time they run. They would be slower and could fail for reasons you don't control.

While in unit tests it's easy to mock API calls, on instrumentation tests it's harder. Ideally, you want to fake as little as possible. This means overriding just the HTTP calls and returning what the server would, like a json response, for example.

So bellow you will find an example using Retrofit, OkHttp MockWebServer and Hilt.

Let's imagine the following is our production NetworkModule:

@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {

    @Singleton
    @Provides
    fun provideConverterFactory(): GsonConverterFactory =
        GsonConverterFactory.create(Gson())

    @Provides
    @Singleton
    fun githubService(
        gsonConverterFactory: GsonConverterFactory
    ): GithubApi = Retrofit.Builder()
        .client( OkHttpClient.Builder().build())
        .baseUrl("https://api.bloco.io/".toHttpUrl())
        .addConverterFactory(gsonConverterFactory)
        .build()
        .create(GithubApi::class.java)
}

We want to replace this Module in our test environment, but we don't want to replace all of it.

So let's make small adjustments in a way that we can extend this class and override what we need for our tests.

@Module
@InstallIn(SingletonComponent::class)
// 1. Open the class for overriding
open class NetworkModule {

    // 2. Declare the method we can change
    open fun baseUrl() = "https://api.bloco.io/".toHttpUrl()

    @Singleton
    @Provides
    fun provideConverterFactory(): GsonConverterFactory =
        GsonConverterFactory.create(Gson())

    @Provides
    @Singleton
    fun githubService(
        gsonConverterFactory: GsonConverterFactory,
    ): GithubApi = Retrofit.Builder()
        .client( OkHttpClient.Builder().build())
        .baseUrl(baseUrl()) // 3. make use of our method instead of hardcoded
        .addConverterFactory(gsonConverterFactory)
        .build()
        .create(GithubApi::class.java)
}

Now we can create a MockNetworkModule by extending the production one, there we deploy the URL for our WebMockServer as well as the responses we want to provide for the requests.

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [NetworkModule::class]
)
class MockNetworkModule: NetworkModule() {

    override fun baseUrl(): HttpUrl {
        return runBlocking(Dispatchers.IO) {
            ExampleMockServer.mockWebServer.start(8080)
            return@runBlocking ExampleMockServer.mockWebServer.url("http://localhost:8080/")
        }
    }
}

class ExampleMockServer {
    companion object {
        private fun requestDispatcher(): Dispatcher {
            return object : Dispatcher() {
                override fun dispatch(request: RecordedRequest): MockResponse {
                    return when(request.path) {
                        "/example" -> MockResponse().setResponseCode(200).setBody("{ \"data\": 2}")
                        else -> MockResponse().setResponseCode(404).setBody("{}")
                    }
                }
            }
        }
        val mockWebServer = MockWebServer().apply {
            dispatcher = requestDispatcher()
        }
    }
}

And it's done! All your requests will now be responded with our custom requestDispatcher().

Notes: Simply using the same Url String between NetworkModule and MockWebserver won't work, you have to use them same HttpUrl object. And while the start of the ExampleMockServer will happen with our MockNetworkModule we still should shutdown the server manually.

@After
fun tearDown() {
  ExampleMockServer.mockWebServer.shutdown()
}

Testing data

When writing tests, you need to create fake objects (database models, for example) to send around. Since you need to fill those objects with fake data, we ported Faker to Java, so you can have realistic fake data.

If you find yourself creating fake objects too often, you probably want to write some factory classes to re-use that code.

In Ruby on Rails, you have libraries like Factory Girl, but I haven't found one for Java/Android. You need to write yours from scratch. Something like:

class PersonFactory(
    private val database: Database?
) {
    private var faker = Faker()

    fun build() {
        Person().apply {
            name = faker.name.name()
            email = faker.internet.email()
            createdAt = faker.time.backward(365)
        }
    }
    fun create() {
        return create(build())
    }
    private fun create(person: Person){
        database?.createPerson(person)
        return person
    }
}

Code coverage

It's a good practice to keep an eye on the code coverage your tests have. You can find blind spots of untested code in your apps, or routinely check if the team is not slacking in their tests.

Android Studio already comes with Code Coverage out of the box, although only for one Build Environment at a time. It uses the Jacoco library under the hood.

You just have to enable it on your gradle file:

buildTypes {
    debug {
        ...
        testCoverageEnabled = true
    }
}

After you run your tests, the reports will be generated in the /build/outputs/reports/coverage/debug/folder.


How to test Android Apps

  1. Introduction
  2. Architecture
  3. Unit Testing
  4. Instrumentation Testing
  5. Other details