Android Testing Starter Kit

Testing is normally the last thing that gets set up in a new software project. Not doing so in the very beginning makes it a herculean task to add it later. Inevitably, everyone just gives up, and tests don’t get written. Then in 18 months someone decides that the codebase is past the point of saving, and they should re-write the whole product again. Maybe the second time around tests make it in.

In Android this is even more difficult because the main component classes rely on a Context, and they need to run in a device/emulator. There’s a great project called Robolectric that helps get around this limitation by essentially mocking the entire OS, allowing you to write JVM unit tests for your app. However, Android also has support for JVM unit tests. So whenever possible you should try to architect your app in a way that allows you to maximize the number of unit tests, and minimize the number of instrumentation tests needed for robust test coverage. Below are some of the strategies we use at Ever to make our app testable. We wrote a sample app to illustrate some of the best practices.

Sample App

The app simply shows the trending gifs from the Giphy API, and allows you to search for gifs.

Android Sample app: https://github.com/ralphpina/AndroidSetup

Model View Presenter

There’s many patterns that have been been implemented to separate pure Java code in an Android app, and therefore make it easy to test. We use the Model-View-Presenter pattern. With it we can separate as much logic into pure Java classes, and out of the Activity/Fragment/Views that require a Context. Android classes with a Context almost always need to be tested under instrumentation. In the sample app the MainActivity has a MainPresenter that takes care of all the logic of fetching the API and updating the state of the view. This means all this logic can be tested using unit tests. Ideally, this will mean that you only have to write instrumentation tests in very specific or mission critical situations. At Ever, only around 1/4 of our tests are instrumentation, yet they take up 3/4 of the CI running time. Thanks to that setup we can run the entire unit test suite before a push to make sure nothing has broken. Doing that with instrumentation tests would be quite painful.

Common Test Folder

When you create a new project, you’ll notice Android Studio creates two test source folders, one for instrumentation tests and another for unit tests:

/app/src/androidTest
/app/src/test

However, this presents a problem — if you want to mock things, you’ll need to create two mocks? You should be able to share your test infrastructure in both suites. So let’s do that.

Create another folder:

/app/src/commonTest

So that Android Studio recognizes it, we must add it to our build.gradle file:

android {
...
    sourceSets {
def commonTestDir = 'src/commonTest/java'
def
commonTestResDir = 'src/commonTest/resources'
test {
java.srcDir commonTestDir
resources.srcDir commonTestResDir
}
androidTest {
java.srcDir commonTestDir
resources.srcDir commonTestResDir
}
}
}

A core problem with testing is being able to mock dependencies. You want to test that an API call triggers an action, but you don’t want to make the actual call.

In this new directory we can put all our mocks and test managers.

/app/src/commonTest
BaseTest.java
/component/TestComponent.java
/module/ApplicationTestModule.java

BaseTest class to share setup

Now that you have a shared test codebase, I like to add a BaseTest class that other test classes can extend. This class can have setup code that will be shared by unit and instrumentation tests. Things like setting up a user, populating mock data, etc.

For example, the following BaseTest would allow us to generate test data in both unit and instrumentation test classes.

Dagger DI

Next, we’ll want to use dependency injection in order to mock the classes we want to exclude from tests. Mocking what and when is a very big subject. However, as a rule, we always mock external dependencies, things like API, DBs, OS features like Permissions, GCM, etc.

Setting up Dagger 2 or DI in general is out of scope of this article, but I’ll go through some of the hacks we use to get it working.

Use a Test Rule

By using a test rule for all your injection setup you can make sure that all the setup happens before the tests begin. You’ll also be able to wrap your rule into the Activity or Service test rules you use for your instrumentation test.

Here is an example of the test rule from our sample project:

Now you’ll be able to use it in unit tests:

Or, you can also use it in instrumentation tests. Notice how we are injecting the read test context. And that we’re using the @Rule keyword in the RuleChain, not the TestComponentRule like above.

Unit vs Instrumentation Test Context Injection

You’ll likely want to handle two main cases in the dependency injection:

  • Injecting your Application subclass in instrumentation tests, in which case you have a real context provided by the OS.
  • Injecting your Application subclass in unit tests, in which you’ll want to instantiate a “fake” class.

Therefore, you’ll want to pass your main ApplicationComponent into your Appliation.java subclass. Notice how we call inject() once again. The reason being that in instrumentation tests Application.onCreate() was called before your test rule instantiated the mock dependencies. You’ll need to wipe them out.

This is set from your test rule:

public class TestComponentRule implements TestRule {
    ...
    private void setupDaggerTestComponentInApplication() {
...
application.setComponent(testComponent);
}
...
}

Named Injections

In regular operation, you want networking calls to be done in a separate thread, and then the data delivered to the main thread. However, during tests you’re mocking out networking and you want to convert these asynchronous operations to synchronous ones. When using RxJava, it is convenient to use the built-in Schedulers they provide. So you’ll want to inject multiple instances of a type. For example, in our sample app, we want to inject our RxJava schedulers so that they are Schedulers.immediate() during tests.

In our module, we’ll want to add the @Named keyword:

@Provides
@Named(MAIN_THREAD)
Scheduler provideMainThread() {
return AndroidSchedulers.mainThread();
}

Then we can use multiple schedulers:

@Inject
@Named(MAIN_THREAD)
Scheduler observerOn;
@Inject
@Named(IO)
Scheduler subscribeOn;

Mocking Stuff

All this DI business seems like a pain, and a lot of boilerplate. But now you can mock all sorts of things. For example, in our Rule, we set all our dependencies to be mocked by default:

this.testClient = mock(GiphyClientImpl.class);
this.userManager = mock(UserManagerImpl.class);

During specific tests we can use the Builder pattern to turn on certain dependencies:

Notice that during tests we’ll never want to use the actual client since it just makes network requests. We may however want to use the real UserManager implementation, for example when testing that implementation. You may have different use cases, but it will be easy to control.

Now we can use our fake UserManager:

And use the real object when we want to test it:

Lastly, we find the best thing to do is to make all these dependencies Interfaces as opposed to Abstract or base Classes. This cuts down on the potential for side effects when using a subclass during testing.

Conclusion

If you’re working in an Android codebase, and someone on your team says something along the lines of

we’ll worry about tests later

that should raise huge red flags. Architecting an app to be testable is not trivial, and it is definitely not the path of least resistance. The sample app we wrote could have been implemented in the one MainActivity in a few hundred lines of code. However, this would make it impossible to test. And will grind development to a halt over time as it becomes harder and harder to modify the code base without breaking things, or when Activities start growing to thousands of lines of code.

A common refrain from Android developers is that testing is super hard in the platform. While it isn’t easy, you can now see that it would be a fool’s errand to use that excuse to avoid the necessary work to make it testable.