Testing Dagger 2 with Espresso, Mockito & Robolectric
Dagger 2 brings us @Component and @Module – annotations which imply less boilerplate code, but unfortunately, make our project less testable. In this article, I will show a project, which is using Dagger 2 and is testable with frameworks like Mockito, Espresso or Robolectric.
Anyone who has ever tried testing an Android project that is using Dagger 1 knows, that this is quite possible to override existing modules and replace them with test ones, e.g.:
1 2 3 4 5 6 | @NonNull @Override protected Object[] getModules() { return new Object[] {new TestMainModule()}; } |
Dagger 2 brings us @Component and @Module, annotations which imply less boilerplate code, but unfortunately, makes our project less testable. In this article, I’d like to show the project which is using Dagger 2 and is testable with frameworks like Mockito, Espresso or Robolectric.
Complete source code can be found on Droids On Roids Github
Making project
So let’s assume we want to make calculator. In this project we have application class, simply activity with view and it’s presenter and calculator class which holds our logic:MainActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | package pl.droidsonroids.calculator.main; public class MainActivity extends AppCompatActivity implements MainView { @Bind(R.id.first_editText) EditText firstEditText; @Bind(R.id.second_editText) EditText secondEditText; @Bind(R.id.result_editText) EditText resultEditText; @Bind(R.id.spinner) Spinner spinner; @Inject MainPresenter presenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } private void init() { ButterKnife.bind(this); CalculatorApplication.getInstance().getMainGraph(this).inject(this); presenter.init(); initSpinner(); } private void initSpinner() { ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.chars, android.R.layout.simple_spinner_item); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); } @Override public void showResult(float result) { resultEditText.setText(String.valueOf(result)); } @Override public void showError() { resultEditText.setText("Error!"); } @OnClick(R.id.ok_button) public void onOkButtonClicked() { presenter.makeCalculation(firstEditText.getText().toString(), secondEditText.getText().toString(), spinner.getSelectedItem().toString()); } @Override protected void onDestroy() { presenter.destroy(); super.onDestroy(); } } |
MainPresenterImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | package pl.droidsonroids.calculator.main; public class MainPresenterImpl implements MainPresenter { public final Calculator calculator; private final MainView view; public Subscription subscription; public MainPresenterImpl(Calculator calculator, MainView view) { this.calculator = calculator; this.view = view; } @Override public void init() { subscription = empty(); } @Override public void makeCalculation(String firstNumber, String secondNumber, String selectedSign) { subscription = calculator.calculate(firstNumber, secondNumber, selectedSign) .subscribe(view::showResult, throwable -> view.showError()); } @Override public void destroy() { subscription.unsubscribe(); } } |
Calculator.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | package pl.droidsonroids.calculator.data; public class Calculator { public float makeCalculation(final float firstNumberFloat, final float secondNumberFloat, final String sign) { float result = 0f; switch (sign) { case Sign.PLUS: result = firstNumberFloat + secondNumberFloat; break; case Sign.MINUS: result = firstNumberFloat - secondNumberFloat; break; case Sign.MULTIPLY: result = firstNumberFloat * secondNumberFloat; break; case Sign.DIVIDE: result = firstNumberFloat / secondNumberFloat; break; } return result; } public Observable<Float> calculate(final String firstNumber, final String secondNumber, final String sign) { return Observable.create(observer -> observer.onNext(makeCalculation(Float.valueOf(firstNumber), Float.valueOf(secondNumber), sign))); } public static class Sign { public static final String PLUS = "+"; public static final String MINUS = "-"; public static final String MULTIPLY = "*"; public static final String DIVIDE = "/"; } } |
There’s also application class in which we’re holding graphs.
CalculatorApplication.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | package pl.droidsonroids.calculator; public class CalculatorApplication extends Application { private static CalculatorApplication sInstance; private AppGraph appGraph; @Override public void onCreate() { super.onCreate(); sInstance = this; appGraph = AppGraph.Initializer.init(this); } public static CalculatorApplication getInstance() { return sInstance; } public MainGraph getMainGraph(MainView view) { return MainGraph.Initializer.init(appGraph, view); } } |
Our application works now as a simple calculator with four operations.
We have our activity class with presenter that we’re injecting into:
1 2 3 4 5 6 7 8 9 10 11 12 | public class MainActivity extends AppCompatActivity implements MainView { @Inject MainPresenter presenter; ... @OnClick(R.id.ok_button) public void onOkButtonClicked() { presenter.makeCalculation(firstEditText.getText().toString(), secondEditText.getText().toString(), spinner.getSelectedItem().toString()); } ... } |
We’d like to do some instrumentation tests with Espresso (no problem), and activity’s unit tests (big problem). Because we want to test only activity behavior in unit test, we’d like to replace injections with mocks. Before we’ll do it, let’s quickly get through dagger modules.
Dependency Injection Modules and Components
In project we’ll have two modules – AppModule (provides dependencies for whole application) and MainModule (provides dependencies for MainActivity) and two components (which are bridges between modules and classes which uses injections) – AppGraph and MainGraph.
Until now nothing’s really different than in the other projects. But if you look at the source code, you’ll see there are additional four classes – TestAppModule, TestAppGraph, TestMainModule and TestMainGraph. That’s the key for module overriding in Dagger 2. We need to create test equivalent for every Dagger class, e.g.:
AppModule.java
1 2 3 4 5 6 7 8 9 10 11 | package pl.droidsonroids.calculator.dagger; @Module public class AppModule { @Provides @Singleton public Calculator provideCalculator() { return new Calculator(); } } |
TestAppModule.java
1 2 3 4 5 6 7 8 9 10 11 | package pl.droidsonroids.calculator.dagger; @Module public class TestAppModule { @Provides @Singleton public Calculator provideCalculator() { return mock(Calculator.class); } } |
In our application class to define which mode we’re using now and to initialize proper graph we need to add boolean variable. After changing this class looks like:
CalculatorApplication.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | package pl.droidsonroids.calculator; public class CalculatorApplication extends Application { private static CalculatorApplication sInstance; private AppGraph appGraph; private boolean isTestMode = false; @Override public void onCreate() { super.onCreate(); sInstance = this; appGraph = AppGraph.Initializer.init(this); } @VisibleForTesting public void initTestMode() { appGraph = TestAppGraph.Initializer.init(this); isTestMode = true; } public static CalculatorApplication getInstance() { return sInstance; } public MainGraph getMainGraph(MainView view) { if (isTestMode) return TestMainGraph.Initializer.init(appGraph); else return MainGraph.Initializer.init(appGraph, view); } } |
Now we can init our test mode and use our activity with test dependencies in test cases:
MainActivityTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package pl.droidsonroids.calculator.main; @RunWith(RobolectricGradleTestRunner.class) @Config(constants = BuildConfig.class, sdk = 21) public class MainActivityTest { private MainActivity activity; @Before public void setUp() throws Exception { ((CalculatorApplication) RuntimeEnvironment.application).initTestMode(); activity = Robolectric.setupActivity(MainActivity.class); verify(activity.presenter).init(); } @Test public void testShowingResult() throws Exception { EditText result = (EditText) activity.findViewById(R.id.result_editText); activity.showResult(12f); assertThat(result.getText().toString()).isEqualTo("12.0"); } } |
Source code:
References:
Libraries used:
About the author
Ready to take your business to the next level with a digital product?
We'll be with you every step of the way, from idea to launch and beyond!
Great article,
Is there any way we can inject the presenter in the test classes i.e. instead of mocking the presenter, we inject it using Dagger 2? We were able to do that with Dagger 1
Thanks
Hi Hai,
thank you for your comment. It’s possible to inject the presenter (or any other class) to test classes, you can see how it can be done on repository branch dev.
Anyway, there’s no need for doing this if you want only to inject presenter. The method presented in article also injects presenter (using MainActivity class), but for doing this it uses modules from test package. TestMainModule tells Dagger that you want to use mocked Presenter. But if you change this override, you can tell Dagger to use original presenter from MainModule.
I hope I explained this a bit more. If you have any questions, feel free to comment.
Thanks,
Paulina Szklarska
Hi, nice article. Given that you are using Robolectric, you could take advantage of the fact that it allows to use a different Application object, getting rid of the isTesting boolean in your production code and allowing a different Application object to inject mocks..
Hi Federico, thank you for your comment (and sorry for my late answer ;)). I’ll definitely try this, since I didn’t know about it. Thanks!
No worries. I landed on your post while writing one of mine. You can check it here: http://fedepaol.github.io/blog/2015/09/05/mocking-with-robolectric-and-dagger-2/ (it also points to a working example on github).