Architecture / Testing

Maintainable Architecture – Testing

Creating a maintainable, flexible codebase is not easy but is an essential part of software engineering. In this series we’ll take a look at a simple, functional weather app and look at some of the issues in its design. We shall then refactor and re-design it to create a codebase which will be easier to maintain, less prone to bugs, and easier to add features to. This series is not going to be a deep dive in to the techniques and technologies that we’re going to use, but will be more an exploration of what benefits they give us. In this article we’ll look at UI testing.

We’ve done a lot of work on tidying our codebase up using various techniques, and in the process I’ve been able to add quite a few unit tests because we’ve separated our components in to their own areas of responsibility, and create a decoupled set of components which are pretty simple to test in isolation. However testing our UI is a little trickier. Our Fragment is now much more focused on UI and seems a good candidate for some testing, but we now have Dagger doing all of our dependency injection, and this means that we’ll need location updates and make a network call to obtain the weather data. The former may be tricky if we want to run our tests on an emulator, and having our tests dependent on potentially flakey network connections makes our tests potentially flakey as well, so is a really bad idea. In our unit tests it has been possible to use mocked collaborators so that we can test each component in isolation from the others, and it would be really good if we could adopt the same principle here. The current architecture permits precisely that as CurrentWeatherFragment only has one key collaborator: ViewModelFactory which is injected by Dagger. If we could inject an alternative implementation of that then that implementation could supply a CurrentWeatherViewModel and its CurrentWeatherLiveData instance which does not use location or network access.

One way that we could do this is to have different Dagger code in the androidTest source tree which provides different implementations of these, but there is actually another neat way that we can do this. However to understand how to do this we first need to understand a little about how Dagger works. Dagger generates a chunk of code which gets added to our project. This code is really a lot of boiler plate that has been inferred from analysing the types of objects that we have declared in our Modules, and creating a dependency graph for these. At runtime it will create all of the necessary dependencies for, for example, our CurrentWeatherViewModel in order to create an instance of it. So thinking about it in this way, it makes a certain amount of sense how Dagger is able to create the necessary objects.

On the other end of the process we also declare which Activities & Fragments require injection. In this case we only have the one Fragment:

@Module
abstract class AndroidBuilder {

    @ContributesAndroidInjector
    abstract fun bindCurrentWeatherFragment(): CurrentWeatherFragment
}

Based upon this Dagger finds the @Inject annotations in CurrentWeatherFragment and builds the necessary injector code (which is an instance of AndroidInjector<CurrentWeatherFragment>) to satisfy these dependencies. For CurrentWeatherFragment it’s only the ViewModelFactory instance but, as we’ve seen, this has its own dependency graph.

The bit that invokes this generated code at runtime is the AndroidInjector.inject() method. What this does is finds an instance of this injector code, and calls it to inject the current Fragment. So if we could intercept this to use a test AndroidInjector implementation, then we could substitute in a mock or test implementation of our dependencies. Internally AndroidInjector uses a service locator pattern to find these injectors. The service locator registry gets initialised at runtime in our Application class:

class WeatherStationApplication : Application(), HasSupportFragmentInjector {

    @Inject
    lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>

    private val weatherStationComponent: WeatherStationComponent by lazy {
        DaggerWeatherStationComponent.builder()
                .application(this)
                .build()
    }

    override fun onCreate() {
        super.onCreate()

        weatherStationComponent.inject(this)
        AndroidThreeTen.init(this)
    }

    override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentDispatchingAndroidInjector
}

The injected fragmentDispatchingAndroidInjector is this registry which gets injected by Dagger. In really simple terms, when we call AndroidInjector.inject() it will find this by traversing to the parent Activity, then to the Application Context. At each stage it check whether the Activity or Application implements HasSupportFragmentInjector and, if so, calls supportFragmentInjector() to obtain the registry. Once it has this registry it can lookup an AndroidInjector instance for the current Fragment. So the trick that we can use here it to substitute our own DispatchingAndroidInjector<Fragment> implementation in fragmentDispatchingAndroidInjector.

The technique that I’m going to use for our Espresso tests is the Testing Robot pattern. Jake Wharton gave a great talk on this which I’d encourage you to take a look at if you’re not familiar with the concept. In a nutshell, it encapsulates the logic for interacting with and verifying the UI components within the robot, and then our tests become much terser, easier to understand and more fluent because all of the usual verbose Espresso stuff is encapsulated within behavioural methods within the robot. By using this a simple test is something like this:

@RunWith(AndroidJUnit4::class)
class CurrentWeatherFragmentTest {

    @Rule
    @JvmField
    val permissionRule: TestRule = GrantPermissionRule.grant(
            Manifest.permission.ACCESS_FINE_LOCATION, 
            Manifest.permission.ACCESS_COARSE_LOCATION
    )
    
    private val dummyWeather = CurrentWeather(
            1f,
            2f,
            "London",
            293f,
            10f,
            180f,
            "Clear",
            "Clear",
            "01d",
            Instant.now()
    )
    .
    .
    .
    @Test
    fun testLocationDisplaysCorrectly() {
        currentWeather {
            weatherChanged(dummyWeather)

            showsLocation(dummyWeather.placeName)
        }
    }
    .
    .
    .
}

The permissionRule is to grant the permissions that are required. Although we will not be using Location services in our test, MainActivity performs the check to see if we have the necessary permissions and only displays CurrentWeatherFragment if it does, hence the need for this.

In the test itself, currentWeather creates the robot instance, and the lambda runs with the robot as a receiver. So setting currentWeather to the dummy item we created be the equivalent of getting updated weather data to our Fragment, and showsLocation() validates that the correct place name is displayed in the UI.

Our robot is implemented like this:

fun currentWeather(func: CurrentWeatherFragmentRobot.() -> Unit) =
        CurrentWeatherFragmentRobot().apply(func)

class CurrentWeatherFragmentRobot {
    private val liveData = testLiveData

    fun weatherChanged(newWeather: CurrentWeather) = liveData.postValue(newWeather)

    fun showsLocation(location: String) {
        onView(withId(R.id.city)).apply {
            check(matches(isDisplayed()))
            check(matches(withText(location)))
        }
    }
}

private val testLiveData = MutableLiveData<CurrentWeather>()

We have our own LiveData object which is has no dependency on either LocationProvider or CurrentWeatherProvider, so allows us to test in isolation from them. But this doesn’t perform any injection and we need to be able to get the same LiveData instance in to the CurrentWeatherFragment instance for this to work.

What is missing here are a couple of things. The first is how we launch MainActivity in the first place, and this is done using an ActivityTestRule:

@RunWith(AndroidJUnit4::class)
class CurrentWeatherFragmentTest {

    @get:Rule
    @Suppress("UNUSED")
    val testRule: ActivityTestRule<MainActivity> = ActivityTestRule<MainActivity>(MainActivity::class.java, true, true) {
    .
    .
    .
}

This will launch our MainActivity and, because of the permission granting that we did earlier, we know that CurrentWeatherFragment will be displayed. But as things stand CurrentWeatherFragment will be injected with the standard app behaviours using LocationProvider and CurrentWeatherProvider. To substitute in our dummy version we do the following:

@RunWith(AndroidJUnit4::class)
class CurrentWeatherFragmentTest {
    private val injector = Injector<Fragment>()

    @get:Rule
    @Suppress("UNUSED")
    val testRule: ActivityTestRule<MainActivity> = object : ActivityTestRule<MainActivity>(MainActivity::class.java, true, true) {
        override fun beforeActivityLaunched() {
            super.beforeActivityLaunched()
            injector.apply {
                injectApplication<WeatherStationApplication> { fragmentInjector ->
                    fragmentDispatchingAndroidInjector = fragmentInjector
                }
                registerCurrentWeatherFragmentInjector()
            }
        }
    }
    .
    .
    .
}

The Injector instance is what will provide a custom DispatchingAndroidInjector instance for us to inject in to our Application – we’ll take a look at this shortly. We need to override beforeActivityLaunched() of our ActivityTestRule to do stuff before the Activity is actually launched. In here we first call injectApplication() on our injector. This takes a lambda which will be invoked with a receiver of our WeatherStationApplication instance and a single argument of the replacement DispatchingAndroidInjector. Inside the lambda we are setting the fragmentDispatchingAndroidInjector property of WeatherStationApplication which replaces the standard DispatchingAndroidInjector, with our own. Finally we call registerCurrentWeatherFragmentInjector() which adds our own AndroidInjector<Fragment> to our replacement DispatchingAndroidInjector instance.

Let’s take a look at Injector to see what it does:

nternal class Injector<T> {
    private val providerMap: MutableMap, Provider>> = mutableMapOf()
    private val dispatchingAndroidInjector: DispatchingAndroidInjector = DispatchingAndroidInjector_Factory.newDispatchingAndroidInjector(providerMap)

    inline fun  injectApplication(
            crossinline initBlock: A.(injector: DispatchingAndroidInjector) -> Unit
    ) {
        (InstrumentationRegistry.getTargetContext().applicationContext as? A)?.apply {
            initBlock(dispatchingAndroidInjector)
        }
    }

    inline fun  registerInjector(crossinline initBlock: F.() -> Unit) {
        val injector = AndroidInjector { fragment ->
            fragment.initBlock()
        }
        val factory: AndroidInjector.Factory = AndroidInjector.Factory { injector }
        providerMap[F::class.java] = Provider { factory }
    }
}

The two fields are what defines our new DispatchingAndroidInjector. It wraps a map which contains our registry of Providers which is initially empty. The injectApplication() method is what we called from our ActivityTestRule, it obtains the application Context, casts it to the specific class that we specify, and then invokes initBlock with the cast application Context as a receiver. What this enables us to do is be able to inject the Application without knowing the specifics of the Application instance itself. In our case WeatherStationApplication has a property named fragmentDispatchingAndroidInjector, but the Injector class remains completely agnostic of that. It is the responsibility of the test case to know that, which is where the lambda in ActivityTestRule has the responsibility. This is quite a neat Kotlin trick for deferring responsibilities around.

The other method is a mechanism for adding an arbitrary AndroidInjector.Factory to the providers map, and we use this to add our CurrentWeatherFragment injector:

internal fun Injector<Fragment>.registerCurrentWeatherFragmentInjector() =
        registerInjector<CurrentWeatherFragment> {
            viewModelFactory = TestViewModelFactory()
        }

private class TestViewModelFactory : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return if (modelClass == CurrentWeatherViewModel::class.java) {
            CurrentWeatherViewModel(testLiveData) as T
        } else {
            throw Exception("Not recognised")
        }
    }
}

registerCurrentWeatherFragmentInjector() is the function that we called in our ActivityTestRule to add a dummy implementation to our providers map, and this uses a similar approach as before to shift the responsibility around. The lambda here knows how to set the viewModelFactory property of CurrentWeatherFragment, and the TestViewModelFactory will create instances of CurrentWeatherViewModel which have the same testLiveData implementation as the robot that we created earlier. So we now have things linked up, and by changing the value of testLiveData in the robot will trigger the observer callback in CurrentWeatherFragment.

While this may all seem quite complex, the amount of code that we needed to add was really quite minimal. Out test itself is compact and readable, the robot can be used for lots of different tests, and Injector is reusable throughout any further tests for different Fragments, Activities, and dependencies (thanks to the deferral of knowledge of specific domains).

It may feel like we’ve taken our improvements as far as they can go, but there is still something of an issue. In the next article we’ll identify this and look at a strategy which builds upon some of the techniques that we’ve already used to provide an even more robust and well-behaved app.

The source code for this article is available here.

© 2018, Mark Allison. All rights reserved.

Copyright © 2018 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.