I test-drive Android apps with some help from the Robolectric testing framework. I also get some help (usually) from the Dagger dependency injector for employing dependency injection in those apps. I define modules in dagger for injecting classes into objects that depend on them, and I define test modules that inject mock versions of those classes when I write automated tests for the objects that depend on them.
As I learned more of the Robolectric and Dagger syntax over time, I could test-drive Android app features faster and faster. I did have an issue, though; in order to inject dependencies in Android, I needed a context. The Context object in Android provides information about the application environment, and it enables app-specific stuff like launching activities and sending/receiving broadcasts.
In the app itself, I could get ahold of the context with getApplicationContext(), or I could rely on the activity or service to act as my context. But what about in the tests? The test class is not a context. To get around this, I used Robolectric’s Robolectric.application (in Robolectric 2.4) or RuntimeEnvironment.application (in Robolectric 3.0) as my stand-in context.
I needed dependency injection pretty ubiquitously throughout all my classes, and since I depended on Robolectric to help with dependency injection, all of my tests relied on Robolectric.
This doesn’t become a noticeable problem until the app (and its test suite) get large.
First of all, Robolectric has to do some app setup to run, so Robolectric tests take a non-negligible amount of time. When every single test takes a non-negligible amount of time,a test suite with a few hundred tests takes minutes to run. When that happens, developers run it fewer times per development cycle. This lengthens the time window for a regression or bug to sneak in before developers notice it.
There’s also the memory leak in Robolectric 3.0. This is a documented issue that the Robolectric team knows about and wants to fix, but at the moment, when a test suite reaches 600-700 Robolectric-assisted tests, the suite starts to hang or run out of memory. I’ve heard a variety of solutions to this problem from different Android development teams:
- break up the test suite and run it in parallel JVMs (doable but not fun)
- bump up the maximum heap size in the build.gradle file (a quick but temporary solution, and not possible on machines without resources to spare)
- start axing tests from the suite (PLEASE DO NOT)
So what can we do? A few development teams have tried separating the logic in their apps from the Android Framework. The developers at Square describe their approach to this in a post about separating logic from UI components. Matthew Dupree tried a similar thing with non-UI app components and wrote about it on his blog right here. The idea is that this type of structure makes the code easier to understand, less dependent on the Android framework interface, and easier to unit-test with vanilla JUnit as opposed to Android-specific testing tools.
My team wanted to try this. When we started out, we loved how the JUnit tests ran like lightning. But within 16 hours of starting the project, we had a problem on our hands: we didn’t know how to inject dependencies into our tests without bumming a context off Robolectric.
It turns out, though, that you can do this without Robolectric. The following example still employs dagger for dependency injection.
First, we’ll use a classic singleton to set up our Object Graph from Dagger:
import dagger.ObjectGraph; public class GraphProvider { private static GraphProvider instance; private ObjectGraph graph; public static GraphProvider getInstance() { if (instance == null) { instance = new GraphProvider(); } return instance; } private GraphProvider() { graph = ObjectGraph.create(); } public void addModules(Object... modules) { graph = graph.plus(modules); } public ObjectGraph getGraph() { return graph; } }
We give the object graph our dagger modules when we create the application, like so:
public class ExampleApplication extends Application{ private ObjectGraph objectGraph; @Override public void onCreate() { super.onCreate(); GraphProvider graphProvider = GraphProvider.getInstance(); graphProvider.addModules(getModules().toArray()); objectGraph = graphProvider.getGraph(); } protected List<Object> getModules() { ArrayList<Object> objects = new ArrayList<>(); objects.add(new ExampleModule(this)); return objects; } public void inject(Object object) { objectGraph.inject(object); } }
Then in the tests, we set up the graph anew with mock versions of our modules. The following lines come from the setUp() method of a test class. We give our ExampleModule a mock application that we can use for our context, plus the TestExampleModule that injects the mocks of all our dependencies:
GraphProvider graphProvider = GraphProvider.getInstance(); graphProvider.addModules(new ExampleModule(mock(ExampleApplication.class)), new TestExampleModule()); graphProvider.getGraph().inject(this);
Inside the implementation class, upon creating the class, we inject its dependencies with the real graph defined in ExampleApplication, like so:
GraphProvider.getInstance().getGraph().inject(this);
This approach got us around our previous usage of Robolectric anytime we needed a context in our tests. As we continue our experiment of filtering logic away from Android activities, fragments, and services, I’m sure we’ll learn much more about testing those classes and making them easier to understand.