Reliable functional tests with Espresso and Dagger

One of the core properties of automated tests is that they should be reliable, meaning that a test should either pass or fail, no matter how many times or in what conditions it is being run. Tests that tend to sometimes fail with no clear reason are usually called flaky, and those are a real problem. At some point the development team would just give up fixing flaky tests time and time again and will stop running them at all - and suddenly we've lost that safety net guarding us against inevitable regressions. While unit tests should generally not allow any room for flakiness by mocking out all dependencies, functional tests are a different story. A classic example would be testing a simple screen that loads data from network - it will fail every time you run it while offline! So how do we go about writing reliable functional tests, that are not affected by network conditions? In this article I'll describe an approach that uses Dagger to create clean and robust functional tests.

So what's Dagger?

Dagger has become a pretty standard tool in many Android developers' arsenals, however, for those who haven't heard - it's a fast dependency injection framework, developed by Square, and specifically optimized for Android. Unlike some of the other popular dependency injectors, Dagger doesn't use reflection and relies on generated code for speed. We'll use Dagger in our app to be able to substitute our dependencies with test doubles in a clean way, without breaking encapsulation or writing unnecessary code that will only be used by tests. So let's get going!

The Weather app

We'll develop a simple one-screen weather app for demonstration purposes. The app should ask the user to provide the name of the city and will download some information about current weather in this city. That's how it looks:

Weather app

The complete source code is available on GitHub.

OpenWeatherMap API

We'll use the OpenWeatherMap API to access weather data. The API is free, however you'll need to sign up for an API key if you want to download the code and compile the app on your machine.

REST API client setup

Let's start with setting up the REST API client that will implement data fetching. We'll be using Retrofit along with RxJava, so the following dependencies go to our build.gradle file:

dependencies {  
    // rest of dependencies

    compile 'com.squareup.retrofit:retrofit:1.9.0'
    compile 'io.reactivex:rxandroid:1.0.1'
}

Next is a simple POJO called WeatherData that will represent the information we fetch from the server:

public class WeatherData {

    public static final String DATE_FORMAT = "EEEE, d MMM";

    private static final int KELVIN_ZERO = 273;

    private static final String FORMAT_TEMPERATURE_CELSIUS = "%d°";
    private static final String FORMAT_HUMIDITY = "%d%%";

    private String name;
    private Weather[] weather;
    private Main main;

    public String getCityName() {
        return name;
    }

    public String getWeatherDate() {
        return new SimpleDateFormat(DATE_FORMAT, Locale.getDefault()).format(new Date());
    }

    public String getWeatherState() {
        return weather().main;
    }

    public String getWeatherDescription() {
        return weather().description;
    }

    public String getTemperatureCelsius() {
        return String.format(FORMAT_TEMPERATURE_CELSIUS, (int) main.temp - KELVIN_ZERO);
    }

    public String getHumidity() {
        return String.format(FORMAT_HUMIDITY, main.humidity);
    }

    private Weather weather() {
        return weather[0];
    }

    private static class Weather {
        private String main;
        private String description;
    }

    private static class Main {
        private float temp;
        private int humidity;
    }
}

And then the simple Retrofit interface, containing the description of a GET request we'll be making to fetch the data:

public interface WeatherApiClient {

    Endpoint ENDPOINT = Endpoints.newFixedEndpoint("http://api.openweathermap.org/data/2.5");

    @GET("/weather") Observable<WeatherData> getWeatherForCity(@Query("q") String cityName);
}

And that's it so far for the networking setup. Let's now set up Dagger and teach it to provide an implementation of WeatherApiClient to the classes that will be using it.

Dagger setup

To add Dagger to your project, add the following lines to build.gradle:

final DAGGER_VERSION = '2.0.2'

dependencies {  
    // Retrofit dependencies are here

    compile "com.google.dagger:dagger:${DAGGER_VERSION}"
    apt "com.google.dagger:dagger-compiler:${DAGGER_VERSION}"
    provided 'org.glassfish:javax.annotation:10.0-b28'
}

You'll notice apt scope used to include the dagger-compiler artefact: since dagger-compiler is an annotation processor, we'll only want to use it at compilation stage and not package it into the APK (dagger-compiler is actually pretty big in terms of dex method count). This can be achieved by using the android-apt plugin. Add the following line to your top-level build.gradle file:

buildscript {  
    dependencies {
        // other classpath declarations
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

And then the following at the top of the app build.gradle file, right below the apply plugin: 'com.android.application':

apply plugin: 'com.neenbedankt.android-apt'  

Now we're all set with the dependencies. Let's proceed with creating a Dagger module, which will describe our logic of providing dependencies:

@Module
public class AppModule {

    private final Context context;

    public AppModule(Context context) {
        this.context = context.getApplicationContext();
    }

    @Provides @AppScope public Context provideAppContext() {
        return context;
    }

    @Provides public WeatherApiClient provideWeatherApiClient() {
        return new RestAdapter.Builder()
                .setEndpoint(WeatherApiClient.ENDPOINT)
                .setRequestInterceptor(apiKeyRequestInterceptor())
                .setLogLevel(BuildConfig.DEBUG ? RestAdapter.LogLevel.FULL : RestAdapter.LogLevel.NONE)
                .build()
                .create(WeatherApiClient.class);
    }

    private RequestInterceptor apiKeyRequestInterceptor() {
        return new ApiKeyRequestInterceptor(context.getString(R.string.open_weather_api_key));
    }
}

As you can see, provideWeatherApiClient() is actually creating and returning the instance of WeatherApiClient: this code will be called by Dagger every time we'll request it to provide us with an instance of WeatherApiClient. Sweet! Now let's add a Component interface, that will describe the contract of the dependency graph of our app, created by Dagger:

@AppScope
@Component(modules = AppModule.class)
public interface AppComponent {

    void inject(MainActivity activity);

    @AppScope Context appContext();

    WeatherApiClient weatherApiClient();
}

The AppComponent is able to provide the instance of the application Context and instances of WeatherApiClient, and it can also inject dependencies into the MainActivity.

Finally, we'll need to instantiate our AppComponent and make it available to other classes. We'll add the following into our custom Application class called WeatherApp:

public class WeatherApp extends Application {

    private AppComponent appComponent;

    @Override
    public void onCreate() {
        super.onCreate();

        appComponent = DaggerAppComponent.builder()
                .appModule(new AppModule(this))
                .build();
    }

    public AppComponent appComponent() {
        return appComponent;
    }
}

Now let's switch to MainActivity and see how we can access WeatherApiClient and fetch weather data.

MainActivity

The relevant parts of the MainActivity are as follows (complete source):

public class MainActivity extends AppCompatActivity implements SearchView.OnQueryTextListener {

    @Inject WeatherApiClient weatherApiClient;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ((WeatherApp) getApplication()).appComponent().inject(this);
    }

    @Override
    public boolean onQueryTextSubmit(String query) {
        if (!TextUtils.isEmpty(query)) {
            loadWeatherData(query);
        }
        return true;
    }

    private void loadWeatherData(String cityName) {
        subscription = weatherApiClient.getWeatherForCity(cityName)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(
                        // handle result
                        }
                );
    }
}

Notice how we instantiate WeatherApiClient: instead of doing it manually we're marking it with an @Inject annotation and doing the following in onCreate():

((WeatherApp) getApplication()).appComponent().inject(this);

By accessing our AppComponent and requesting it to inject the MainActivity we're asking Dagger to satisfy all the dependencies, marked with @Inject, which it successfully does. Now we can use WeatherApiClient to fetch the data.

Although this approach looks verbose and anything else but straightforward at first glance, the power is in the fact that we don't hardcode the logic of creating dependencies in the code itself. This will prove very useful when we'll need to substitute the dependencies in test mode, which is our next step.

Espresso setup

Let's now integrate Espresso into our project and write a test to verify that we can correctly load and display weather data. First, add the following to the build.gradle file:

final ESPRESSO_VERSION = '2.2.1'  
final ESPRESSO_RUNNER_VERSION = '0.4'

dependencies {  
    // 'compile' dependencies

    androidTestCompile "com.android.support.test:runner:${ESPRESSO_RUNNER_VERSION}"
    androidTestCompile "com.android.support.test:rules:${ESPRESSO_RUNNER_VERSION}"
    androidTestCompile "com.android.support.test.espresso:espresso-core:${ESPRESSO_VERSION}"
    androidTestApt "com.google.dagger:dagger-compiler:${DAGGER_VERSION}"
}

Notice that we also need to mention dagger-compiler here, since our test code must also be processed by the annotation processor. And then let's add the following test class:

@LargeTest
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

    private static final String CITY_NAME = "München";

    @Rule public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class);

    @Inject WeatherApiClient weatherApiClient;

    @Before
    public void setUp() {
        weatherApiClient = ((WeatherApp) activityTestRule.getActivity().getApplication()).appComponent()
                .weatherApiClient();
    }

    @Test
    public void correctWeatherDataDisplayed() {
        WeatherData weatherData = weatherApiClient.getWeatherForCity(CITY_NAME).toBlocking().first();

        onView(withId(R.id.action_search)).perform(click());
        onView(withId(android.support.v7.appcompat.R.id.search_src_text)).perform(replaceText(CITY_NAME));
        onView(withId(android.support.v7.appcompat.R.id.search_src_text)).perform(pressKey(KeyEvent.KEYCODE_ENTER));

        onView(withId(R.id.city_name)).check(matches(withText(weatherData.getCityName())));
        onView(withId(R.id.weather_date)).check(matches(withText(weatherData.getWeatherDate())));
        onView(withId(R.id.weather_state)).check(matches(withText(weatherData.getWeatherState())));
        onView(withId(R.id.weather_description)).check(matches(withText(weatherData.getWeatherDescription())));
        onView(withId(R.id.temperature)).check(matches(withText(weatherData.getTemperatureCelsius())));
        onView(withId(R.id.humidity)).check(matches(withText(weatherData.getHumidity())));
    }
}

The test case is pretty straightforward: we want to load the data for a certain city and verify that it's displayed correctly. It should work fine in most cases, but imagine running it on a device in Airplane mode - it will most definitely fail! Since our test case is designed to verify that the app displays the data correctly, lack of data due to inability to connect to the network is not a valid scenario to make the test case fail. In addition, we'll probably want to write another test case that checks that our app behaves well in Airplane mode, showing a meaningful error to the user, - how do we make both test cases pass at the same time? Dagger to the rescue! Let's leverage the power of dependency injection and provide an implementation of WeatherApiClient that we could configure with data that we expect to receive.

MockWeatherApiClient

A solution we're looking for is a WeatherApiClient that just returns hardcoded data. Let's create a TestData class that will store a JSON representation of the response we're expecting to receive:

public final class TestData {

    public static final String MUNICH_WEATHER_DATA_JSON = "\n" +
            "{\n" +
            "    \"coord\": {\n" +
            "        \"lon\": 11.58,\n" +
            "        \"lat\": 48.14\n" +
            "    },\n" +
            "    \"weather\": [{\n" +
            "        \"id\": 741,\n" +
            "        \"main\": \"Fog\",\n" +
            "        \"description\": \"fog\",\n" +
            "        \"icon\": \"50n\"\n" +
            "    }],\n" +
            "    \"base\": \"cmc stations\",\n" +
            "    \"main\": {\n" +
            "        \"temp\": 275.68,\n" +
            "        \"pressure\": 1030,\n" +
            "        \"humidity\": 93,\n" +
            "        \"temp_min\": 274.15,\n" +
            "        \"temp_max\": 277.15\n" +
            "    },\n" +
            "    \"wind\": {\n" +
            "        \"speed\": 1.5,\n" +
            "        \"deg\": 240\n" +
            "    },\n" +
            "    \"clouds\": {\n" +
            "        \"all\": 0\n" +
            "    },\n" +
            "    \"dt\": 1449350400,\n" +
            "    \"sys\": {\n" +
            "        \"type\": 1,\n" +
            "        \"id\": 4887,\n" +
            "        \"message\": 0.0134,\n" +
            "        \"country\": \"DE\",\n" +
            "        \"sunrise\": 1449298092,\n" +
            "        \"sunset\": 1449328836\n" +
            "    },\n" +
            "    \"id\": 6940463,\n" +
            "    \"name\": \"Altstadt\",\n" +
            "    \"cod\": 200\n" +
            "}";

    private TestData() {
        // no instances
    }
}

The MockWeatherApiClient will just parse the JSON and return the data. We'll also add some delay to emulate network latency:

public class MockWeatherApiClient implements WeatherApiClient {

    @Override public Observable<WeatherData> getWeatherForCity(String cityName) {
        WeatherData weatherData = new Gson().fromJson(TestData.MUNICH_WEATHER_DATA_JSON, WeatherData.class);
        return Observable.just(weatherData).delay(1, TimeUnit.SECONDS);
    }
}

By having a configurable WeatherApiClient we no longer depend on any external conditions, we can configure the client to return any data we want to test against. Let's now figure out how to make our test work with MockWeatherApiClient, rather than the real client that our app is using.

Dagger test setup

We'll need to mirror the setup we've already performed in our app code, so let's start with creating the TestAppModule:

@Module
public class TestAppModule {

    private final Context context;

    public TestAppModule(Context context) {
        this.context = context.getApplicationContext();
    }

    @Provides @AppScope public Context provideAppContext() {
        return context;
    }

    @Provides public WeatherApiClient provideWeatherApiClient() {
        return new MockWeatherApiClient();
    }
}

This class looks pretty much like AppModule, but instead of creating the real WeatherApiClient implementation through Retrofit, it simply instantiates our MockWeatherApiClient. Let's now add the TestAppComponent:

@AppScope
@Component(modules = TestAppModule.class)
public interface TestAppComponent extends AppComponent {

    void inject(MainActivityTest test);
}

TestAppComponent extends AppComponent and adds an inject() method that our test class will use. Let's change the setUp() method in the test class to the following:

@Before
public void setUp() {  
    ((TestWeatherApp) activityTestRule.getActivity().getApplication()).appComponent().inject(this);
}

The last thing is to substitute the WeatherApp with a test double:

public class TestWeatherApp extends WeatherApp {

    private TestAppComponent testAppComponent;

    @Override
    public void onCreate() {
        super.onCreate();

        testAppComponent = DaggerTestAppComponent.builder()
                .testAppModule(new TestAppModule(this))
                .build();
    }

    @Override
    public TestAppComponent appComponent() {
        return testAppComponent;
    }
}

Notice that we're now returning a TestAppComponent instead of the AppComponent. The interface of the class remains the same, meaning that the app code will have no idea it's working against a test double.

We're all done with the Dagger setup, but a crucial piece is missing: how do we make our tests use TestWeatherApp instead of WeatherApp? The answer is - by using a custom test runner!

Implementing a custom test runner

The AndroidJUnitRunner used to run Espresso tests has a convenient method called newApplication(), that we can override to substitute WeatherApp with TestWeatherApp:

public class WeatherTestRunner extends AndroidJUnitRunner {

    @Override
    public Application newApplication(ClassLoader cl, String className, Context context) throws InstantiationException,
            IllegalAccessException, ClassNotFoundException {
        String testApplicationClassName = TestWeatherApp.class.getCanonicalName();
        return super.newApplication(cl, testApplicationClassName, context);
    }
}

Also don't forget to declare the new test runner in build.gradle:

defaultConfig {  
    // rest of configuration

    testInstrumentationRunner "me.egorand.weather.runner.WeatherTestRunner"
}

And that's it! We can run the tests with the following command:

./gradlew connectedAndroidTest

We now have a setup that will allow us to run our functional tests independent of the network condition and be sure that they'll pass! Check the complete source code for this article on GitHub.

Conclusion

As I've mentioned in Testing a sorted list with Espresso, having a suite of acceptance tests is a great way to catch regressions and make sure most bugs are caught by your development team, not by your users. It's important to make sure your tests are reliable: flaky tests just waste your team's time on fixing them again and again, until everyone decides to stop running them at all.

By using Dagger we can decouple the logic of dependency injection from our code, which allows us to use test doubles and control some of the aspects of our application under test. This article describes using such technique to allow running network-related tests in offline mode and making sure they pass. It's worth mentioning though, that this approach can't be used if your tests are designed as end-to-end tests, since we're not testing the app in real conditions, just like your users will. Still, it's a perfectly valid approach for functional tests, and gives you a lot of flexibility in testing different aspects of your application logic.

Are you using Dagger in your Espresso tests? I'd love to hear about different approaches to functional testing with Espresso. If you have any questions or observations - please don't hesitate to drop me a note! Cheers!