Writing Testable Android MVVM App: Part 3. Dagger 2
In the previous post, we looked at how we can use ViewModels in RecyclerView, and gave unit testing a shot. However, we ran into a few issues during unit testing. In this post, we’ll explore how we can have a more testable architecture.
The source code for this post is tagged as part3 and available on GitHub.
Let’s look at MainViewModelTest again.
The problem above is that the tests are dependent on the Intent object working correctly. Ideally, we shouldn’t have to care about the Intent object or how it works, since it’s an Android Framework object.
All we really should care about is that when you click on a button, it performs the expected action — starts ClickCountActivity, starts AndroidVersionsActivity, or opens the correct URL. This is exactly what we should be testing. So how do we do that?
There are many ways to do this (using Interactor, Event Bus, or Command) but the idea is the same. We want to abstract out the details and expose only the interface we want. Let’s create AttachedActivity interface, which the ViewModel can use to perform Activity related actions.
This will allow us to do things like:
and test it like:
So how do we implement AttachedActivity and tie it back to the ViewModels so they can use it?
The last thing we want is to have a strong reference to an Activity in our ViewModel. Fortunately, we have Dagger 2, which we can leverage to create an Activity scope dependency and inject the Activity. You can read more about scoped dependency in this post by Fernando Cejas or in this post by Miroslaw Stanek.
MvvmApp Library with AttachedActivity
So what’s this going to look like? As discussed above, we’ll create:
And then we’ll add a few more things to use Dagger 2:
And finally we’ll update the existing components to use the newly added components we added.
This is just a plain interface, as previously shown.
This is the implementation of the AttachedActivity that will get used. We’ll use a WeakReference here to make sure we don’t leak the Activity. We could have used Activity instead of ViewModelActivity here, but I thought I might use some ViewModelActivity specific methods later, so it’s up to you.
This is how we’ll define the Activity scope I mentioned above.
Just a standard Dagger 2 module which will provide the AttachedActivity.
And the Dagger2 component with Activity scope.
Each ViewModel will now inject AttachedActivity via the ActivityComponent that’s passed in.
The ViewModelActivity will now create the ActivityModule and provide the ActivityComponent to the ViewModel.
And the corresponding changes for the Fragment version. Note that ViewModel creation now happens in onActivityCreated() instead, since the ActivityComponent will only get initialized after onCreate() is called on the ViewModelActivity. Since that happens after Fragment.onCreateView(), the method signature has been changed to createAndBindViewModel(), to clearly express that the ViewModel should be bound in this method.
Sample App with AttachedActivity
Now let’s see how the Sample App will change with the updated MvvmApp library.
With AttachedActivity injected, we don’t have to pass in the Activity anymore. However, we don’t have a way to getString() yet, so we’ll revisit this a bit later and comment it out for now.
MainActivity becomes even simpler.
onCreateView() will just inflate and return the View, and the binding will happen in createAndBindViewModel().
Unit Test with AttachedActivity
Now that we are injecting AttachedActivity, let’s see how that changes unit testing. We’ll add:
We’ll use Mockito to mock the AttachedActivity in here. Note that instead of using @PerActivity, we’re using @Singleton here. This will allow the test to access the same instance that the class under test is using.
We’ll extend ActivityComponent and make it use the TestModule, and also allow BaseTest to be injected with AttachedActivity.
This will serve as the base for all our unit test. By injecting AttachedActivity with TestComponent, all our tests will now have access to the same mocked AttachedActivity instance that the class under test is using.
Let’s take a quick peek at MainViewModelTest. Now that we’re using the injected AttachedActivity, we can simply verify that startActivity is called with the expected target Activity class.
MvvmApp Library with AppContext
One way to handle the getString() is to inject the Application Context. Similar to how we did Activity scoping, we’ll add an Application scope (basically a Singleton) and guard against using the wrong context by creating a Qualifier. We’ll add the following to MvvmApp library:
Not only is this to make sure that we’re not passing around Activity context (thereby leaking Activities), it’s also helpful because Context behaves differently, which Dave Smith has a great post about.
This module will provide the application Context.
And the AppComponent that’ll use the AppModule. Since we want to make the application Context available to ActivityComponent as well, we’ll expose it to the sub-graph.
Lastly, we’ll create a base Application that will build the AppComponent.
Now we can make ActivityComponent a sub-graph of the AppComponent.
ViewModelActivity / ViewModelFragment
And supply the AppComponent when building the ActivityComponent.
And now the application context is available in the ViewModel.
Sample App with AppContext
Let’s take advantage of the AppContext in the Sample App!
First of all, we’ll have to extend from MvvmApplication and update the manifest file.
Now we can update MainViewModel to use the AppContext.
Unit Test with AppContext
Let’s see how the unit tests will change.
The TestModule can now provide a mock Context.
Inject and reset AppContext.
Now we can just check openUrl() was called with R.string.twitter_url, and it doesn’t even matter what the value of R.string.twitter_url is, which is what we want.
Binding and Rotation
It’s great that we have a testable architecture now, but is this enough? Let’s take a look at how we might test ClickCountViewModel.
If we forgot to notify that a property changed in the ViewModel, the View wouldn’t update, so we probably want to test this somehow. Likewise, one of my original goals was to be able to test configuration change, which we don’t have below.
So how do we test that the property changed and everything was restored properly after rotating? We’ll explore this in the next post, Writing Testable Android MVVM App: Part 4. ViewModelTest
I’d love to hear your thoughts! Please leave your response below, or feel free to just say hi on Twitter @hiBrianLee.
Other parts of this post:
- Writing Testable Android MVVM App: Prelude
- Writing Testable Android MVVM App: Part 1. ViewModel
- Writing Testable Android MVVM App: Part 2. RecyclerView
- Writing Testable Android MVVM App: Part 3. Dagger 2
- Writing Testable Android MVVM App: Part 4. ViewModelTest
- Writing Testable Android MVVM App: Part 5. Espresso
Some of my other posts: