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.

The Problem

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?

Abstraction

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?

Dagger 2

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:

  • AttachedActivity
  • AttachedViewModelActivity

And then we’ll add a few more things to use Dagger 2:

  • PerActivity
  • ActivityModule
  • ActivityComponent

And finally we’ll update the existing components to use the newly added components we added.

AttachedActivity
This is just a plain interface, as previously shown.

AttachedViewModelActivity
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.

PerActivity
This is how we’ll define the Activity scope I mentioned above.

ActivityModule
Just a standard Dagger 2 module which will provide the AttachedActivity.

ActivityComponent
And the Dagger2 component with Activity scope.

ViewModel
Each ViewModel will now inject AttachedActivity via the ActivityComponent that’s passed in.

ViewModelActivity
The ViewModelActivity will now create the ActivityModule and provide the ActivityComponent to the ViewModel.

ViewModelFragment
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.

MainViewModel
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
MainActivity becomes even simpler.

AndroidVersionsFragment
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:

  • TestModule
  • TestComponent
  • BaseTest

TestModule
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.

TestComponent
We’ll extend ActivityComponent and make it use the TestModule, and also allow BaseTest to be injected with AttachedActivity.

BaseTest
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.

MainViewModelTest
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:

  • AppContext
  • AppModule
  • AppComponent
  • MvvmApplication

AppContext
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.

AppModule
This module will provide the application Context.

AppComponent
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.

MvvmApplication
Lastly, we’ll create a base Application that will build the AppComponent.

ActivityComponent
Now we can make ActivityComponent a sub-graph of the AppComponent.

ViewModelActivity / ViewModelFragment
And supply the AppComponent when building the ActivityComponent.

ViewModel
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!

MvvmSampleApplication
First of all, we’ll have to extend from MvvmApplication and update the manifest file.

MainViewModel
Now we can update MainViewModel to use the AppContext.

Unit Test with AppContext

Let’s see how the unit tests will change.

TestModule
The TestModule can now provide a mock Context.

BaseTest
Inject and reset AppContext.

MainViewModelTest
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.

ClickCountViewModelTest
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