Let’s imagine we have a simple Android app that interacts with user inputs, communicates with REST API server and shows some data back to the user. The code is well structured and decoupled with using Dagger 2 dependency injection framework. The app contains only one screen with SearchView for user input and RecyclerView for server output.

Our task is to write an instrumented UI test to prove that our app works correctly.

Introduction

Before we dig into the task, let’s look at the code structure, dependencies, and patterns used in this project.

Dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object.

Dagger 2 is a fully static, compile-time dependency injection framework for both Java and Android. If you are not familiar with Dagger 2 please read the documentation. It’s not in my intent to explain to you how this library works. I want to focus more on the right implementation and testing. Although you can write an android app in Java, many of the patterns commonly applied to code intended for Android are contrary to those applied to other Java code. When we look at the following code, we can see the problem. This implementation breaks a core principle of dependency injection.

A class shouldn’t know anything about how it is injected.

Dagger Android is a part of the Dagger 2 library that can help you solve this problem. It provides useful classes like DaggerApplication, DaggerActivity or DaggerFragment witch implements AndroidInjection to inject the dependencies under the hood. The same activity may looks as follows:

Best practice how to keep source code clean, readable and easy to test is to have a well-structured code divided into multiple modules, where each module is responsible for different part of the app like network communication, data storage, user interface, etc.

User interface testing lets you ensure that your app meets its functional requirements and achieves a high standard of quality such that it is more likely to be successfully adopted by users.

You could have heard that automated UI tests are flaky, fragile and unreliable. Especially when you are testing the app working with 3rd party API service. Sure, the network connection is unreliable and server responses are not in your hands. Luckily when we have multiple decoupled modules we can just swap network module out for a fake network module during the test to simulate the behavior of API services and have constant responses to certain requests.

MockWebServer is handy library from square contains scriptable web server for testing HTTP clients. It allows you to specify responses to app requests, simulate server errors, slow loading, and more useful things. You can find more info about this library in the documentation.

A test runner is a tool that executes tests and shows you test results.

The Espresso testing framework is an instrumentation-based API and works with the AndroidJUnitRunner test runner. Espresso has basically three main components:

ViewMatchers allow finding the view in the current view hierarchy

ViewActions allow performing actions on the views

ViewAssertions allow asserting state of a view

Base Espresso test looks as follows:

onView(ViewMatcher)

.perform(ViewAction)

.check(ViewAssertion);

UI Test

To enable Dagger 2 annotation processor in androidTest folder we need to add a few lines to build.gradle.

kaptAndroidTest "com.google.dagger:dagger-compiler:$dagger_version"

kaptAndroidTest "com.google.dagger:dagger-android-processor: $dagger_version"

Next, we need to create a fake network module that will connect to MockWebServer instead of the real API server. We don’t even need to change the whole module only the part that provides base URL to API server.

As you can see in the following code snippet we created MockUrlModule which provides MockWebServer instance and url to this server. Because Android doesn’t allow network operations on main thread we need to perform these operations on another thread. We still block main thread because we are waiting until these threads finish but we don’t need to worry about it because this code will be executed only in the testing environment. You can use also asynchronous approach if you want cleaner solution but you will need to change also production code in that case.

We create TestAppComponent that extend our AppComponent and replace original UrlModule::class out for MockUrlModule::class. We want a direct access to a mock web server from the testing environment so we add an abstract function getMockWebServer(). We don’t need to worry about implantation. Dagger will create DaggerTestAppComponent that implements this interface for us.

And we can finally write our first instrumented test. If you are not familiar with UI test I recommend you to read the android documentation.

As you can see on the line number 5 in the following snippet, we create ActivityTestRule for the MainActivity under test. We want to start the activity in touch mode but not automatically, because we want to manually inject dependencies. Next, we create an injector and launch activity.

In the first test, we find out whether the app can handle server error or not. The app should show snackbar with an error message. We setup how mock web server should respond to our request and simulate user input. Espresso doesn’t directly support SearchView, so we have to write custom ViewAction for this View as we can see on the line number 61. Test waits for 1s and after then check if snackbar with error message appears on the screen. Sleep is not the best way how to wait for the network response, but until we use our mock web server we don’t need to worry about it. The better way is to use Idling Resource but we will need to change also production code in that case.

In the second test, we find out whether the app shows a correct number of items in RecyclerView. Again we setup mock web server and simulate user input. To find out a number of items in RecyclerView we need to implement custom matcher. The matcher is implemented in the following snippet.

Now we can connect our android device and run the test.