Android testing with dagger

Android testing using Dagger 2, Mockito and a custom JUnit rule

Jan 31, 2016 · 6 min read

Dependency injection is a key concept to get testable code. Using dependency injection it’s easy to replace an object with a mock to change and verify the behavior of a system.

Dagger 2 is the dependency injection library used in many Android projects, in this post we’ll see how to take advantage of it to test an Android app.

Let’s see a very simple example, the MainService class uses other two classes to simulate a call to an external service and print the result:

The doSomething m ethod has no direct input and output but, thanks to dependency injection and Mockito, it’s not difficult to test it.

The implementation of the other classes is straightforward:

We want to test the MainService class in isolation, for this reason we don’t use Inject annotation in these two classes (we’ll see more details later in this post). We instantiate them in a Dagger module:

We need a Dagger component to instantiate the MainService object and inject the Activity:

JUnit test using Mockito

The MainService class can be easily tested in isolation using Mockito:

The MockitoRule usage is equivalent to the MockitoJUnitRunner, it invokes the static method MockitoAnnotations.initMocks to populate the annotated fields. Thanks to the InjectMocks annotation the mainService object is automatically created, the two mocks defined in the test are used as constructor arguments.

Dagger is not involved at all in this kind of test, this can be good because the test is very simple and it’s a real unit test.

Dagger 2 test

Sometimes we want to write an higher level test that uses Dagger to instantiate the objects. The simplest way to override an object is explained in this post written by Artem Zinnatullin. Following his suggestion we can define a TestModule that extends the original module and overrides the methods to return two mocks:

We also need a TestComponent to inject the test object:

The test class contains three fields annotated with Inject annotation, in the setUp method we create the TestComponent and we use it to inject the test object to populate these fields:

This test works correctly but there are some aspects that can be improved:

  • the restService and myPrinter fields contain two mocks but are annotated with Inject and not with Mock as in the previous test;
  • a test module and a test component are required to write and execute the test.

DaggerMock: a JUnit rule to override Dagger 2 objects

Dagger uses an annotation processor to analyze all the classes in the project looking for annotations, but the TestModule of the previous example doesn’t contain any Dagger annotation!

The basic idea of DaggerMock is to create a JUnit rule that dynamically creates a module subclass. The methods of this module subclass return the mocks defined in the test object. It’s not easy to explain it, let’s see the final result:

In this example the rule dynamically creates a MyModule subclass that returns the mocks defined in the test instead of the real objects. This test is similar to the first test in this post (the one with InjectMocks annotation), the main difference is that now we are creating mainService field using Dagger. Other benefits of the DaggerMockRule usage are:

  • not all dependencies of the tested object must be defined in the test. The object defined in the Dagger config is used when a dependent object is not defined in the test;
  • it’s easy to override an object that it’s not directly used (for example when an object A references an object B that references an object C and we want to override just object C).

Espresso test

There are many posts about Dagger, Mockito and Espresso integration. For example this post written by Chiu-Ki Chan contains the most common solution to this problem.

Let’s see another example, an Activity that invokes the method of the previous example:

We can test this Activity using an ActivityTestRule, the test is similar to MainServiceDaggerTest (a TestComponent and a TestModule are used):

DaggerMock and Espresso

This test can be simplified using a DaggerMockRule, in the lambda expression we set the component in the application to override Dagger objects using the mocks:

The rule can be used in a Robolectric test too, an example is available in the project repository.

Custom rule

The same rule is often used in all the tests of a project, we can create a subclass to avoid copy and paste. For example the rule of the previous example can be written in a new class MyRule:

In some cases we want to override an object but we don’t need the reference in the test. For example in an Espresso test we don’t want to track the analytics events to a remote server, we can use a mock to fix it. To define a custom object we can invoke one of the following methods on the rule:

  • provides(Class originalClass, T newObject): override the object of a class with a specific object;
  • provides(Class originalClass, Provider provider): similar to the previous method but useful for non singleton objects;
  • providesMock(Class … originalClasses): overrides using a mock all the objects of the classes passed as arguments. It’s a shorthand for provide(MyObject.class, Mockito.mock(MyObject.class)).

An example of a custom rule that uses these methods is available in the CoseNonJaviste app:

The final version of the Espresso test is very simple (and you don’t need a TestComponent or a TestModule!):

Читайте также:  Как найти выключенный андроид дома

DaggerMock is an open source project available on GitHub, you can easily integrate it in your project using the JitPack repository.

Источник

Testing with Dagger

One of the benefits of using dependency injection frameworks like Dagger is that it makes testing your code easier. This document explores some strategies for testing applications built with Dagger.

Replace bindings for functional/integration/end-to-end testing

Functional/integration/end-to-end tests typically use the production application, but substitute fakes (don’t use mocks in large functional tests!) for persistence, backends, and auth systems, leaving the rest of the application to operate normally. That approach lends itself to having one (or maybe a small finite number) of test configurations, where the test configuration replaces some of the bindings in the prod configuration.

Separate component configurations

The recommended approach is to have a separate component configuration for each environment (e.g. production and testing). The testing component type extends the production component type so that it gets all of the same entry point and corresponding interfaces, but it installs a different set of modules.

Now the main method for your test binary calls DaggerTestComponent.builder() instead of DaggerProductionComponent.builder() . Note that the test component interface can add provision handles to the fake instances ( fakeAuthManager() and fakeFooService() ) so that the test can access them to control the harness if necessary.

Do not override bindings by subclassing modules

You might think a simple way to replace bindings in a testing component is to override modules’ @Provides methods in a subclass. Then, when you create an instance of your Dagger component, you can pass in instances of the modules it uses. (You do not have to pass instances for modules with no-arg constructors or those without instance methods, but you can.) For example:

But there are problems with this approach:

Using a module subclass cannot change the static shape of the binding graph: it cannot add or remove bindings, or change bindings’ dependencies. Specifically:

Overriding a @Provides method can’t change its parameter types, and narrowing the return type has no effect on the binding graph as Dagger understands it. In the example above, testingComponent still requires a binding for AuthManagerImpl and all its dependencies, even though they are not used.

Similarly, the overriding module cannot add bindings to the graph, including new multibinding contributions (although you can still override a SET_VALUES method to return a different set). Any new @Provides methods in the subclass are silently ignored by Dagger. Practically, this means that your fakes cannot take advantage of dependency injection.

@Provides methods that are overridable in this way cannot be static, so their module instances cannot be elided. This will affect runtime performance of your production code as well.

This method is brittle and usually makes code changes difficult. Most users won’t expect the @Provides methods to be overridden by a test, and so adding new dependencies will break tests even when it would be a functional no-op in Dagger.

Organize modules for testability

Module classes are a kind of utility class: a collection of independent @Provides methods, each of which may be used by the injector to provide some type used by the application.

(Although several @Provides methods may be related in that one depends on a type provided by another, they typically do not explicitly call each other or rely on the same mutable state. Some @Provides methods do refer to the same instance field, in which case they are not in fact independent. The advice given here treats @Provides methods as utility methods anyway because it leads to modules that can be readily substituted for testing.)

So how do you decide which @Provides methods should go together into one module class?

One way to think about it is to classify bindings into published bindings and internal bindings, and then to further decide which of the published bindings has reasonable alternatives.

Published bindings are those that provide functionality that is used by other parts of the application. Types like AuthManager or User or DocDatabase are published: they are bound in a module so that the rest of the application can use them.

Internal bindings are the rest: bindings that are used in the implementation of some published type and that are not meant to be used except as part of it. For example, the bindings for the configuration for the OAuth client ID or the OAuthKeyStore are intended to be used only by the OAuth implementation of AuthManager , and not by the rest of the application. These bindings are usually for package-private types or are qualified with package- private qualifiers.

Some published bindings will have reasonable alternatives, especially for testing, and others will not. For example, there are likely to be alternative bindings for a type like AuthManager : one for testing, others for different authentication/authorization protocols.

But on the other hand, if the AuthManager interface has a method that returns the currently logged-in user, you might want to publish a binding that provides User by simply calling getCurrentUser() on the AuthManager . That published binding is unlikely to ever need an alternative.

Once you’ve classified your bindings into published bindings with reasonable alternatives, published bindings without reasonable alternatives, and internal bindings, consider arranging them into modules like this:

One module for each published binding with a reasonable alternative. (If you are also writing the alternatives, each one gets its own module.) That module contains exactly one published binding, as well as all of the internal bindings that that published binding requires.

All published bindings with no reasonable alternatives go into modules organized along functional lines.

The published-binding modules should each include the no-reasonable-alternative modules that require the public bindings each provides.

It’s a good idea to document each module by describing the published bindings it provides.

Here’s an example using the auth domain. If there is an AuthManager interface, it might have an OAuth implementation and a fake implementation for testing. As above, there might be an obvious binding for the current user that you wouldn’t expect to change among configurations.

Читайте также:  Navitel ломаный для андроида

Then your production configuration will use the real modules, and the testing configuration the fake modules, as described above.

Unit tests and manual instantiation unit tests

For smaller unit tests, it might seem like a good idea to just avoid using Dagger entirely and just instantiate objects by calling the @Inject constructor. However, there are downsides to this approach. See this [testing philosophy] discussion for those downsides.

It is generally recommended to create a Dagger component in your tests to instantiate objects, whether that is a larger test component for many tests or a small focused test component for the individual test.

Источник

Testing

Introduction

Note: Currently, Hilt only supports Android instrumentation and Robolectric tests (although, see here for limitations when running Robolectric tests via Android Studio). In addition, Hilt cannot be used in vanilla JVM tests, but it does not prevent you from writing these tests as you would normally.

Hilt makes testing easier by bringing the power of dependency injection to your Android tests. Hilt allows your tests to easily access Dagger bindings, provide new bindings, or even replace bindings. Each test gets its own set of Hilt components so that you can easily customize bindings at a per-test level.

Many of the testing APIs and functionality described in this documentation are based upon an unstated philosophy of what makes a good test. For more details on Hilt’s testing philosophy see here.

Test Setup

Note: For Gradle users, make sure to first add the Hilt test build dependencies as described in the Gradle setup guide.

To use Hilt in a test:

  1. Annotate the test with @HiltAndroidTest ,
  2. Add the HiltAndroidRule test rule,
  3. Use HiltTestApplication for your Android Application class.

Note that setting the application class for a test (step 3 above) is dependent on whether the test is a Robolectric or instrumentation test. For a more detailed guide on how to set the test application for a particular test environment, see Robolectric testing or Instrumentation testing. The remainder of this doc applies to both Robolectric and instrumentation tests.

If your test requires a custom application class, see the section on custom test application.

If your test requires multiple test rules, see the section on Hilt rule order to determine the proper placement of the Hilt rule.

Accessing bindings

A test often needs to request bindings from its Hilt components. This section describes how to request bindings from each of the different components.

Accessing SingletonComponent bindings

An SingletonComponent binding can be injected directly into a test using an @Inject annotated field. Injection doesn’t occur until calling HiltAndroidRule#inject() .

Accessing ActivityComponent bindings

Requesting an ActivityComponent binding requires an instance of a Hilt Activity . One way to do this is to define a nested activity within your test that contains an @Inject field for the binding you need. Then create an instance of your test activity to get the binding.

Alternatively, if you already have a Hilt activity instance available in your test, you can get any ActivityComponent binding using an EntryPoint .

Accessing FragmentComponent bindings

A FragmentComponent binding can be accessed in a similar way to an ActivityComponent binding. The main difference is that accessing a FragmentComponent binding requires both an instance of a Hilt Activity and a Hilt Fragment .

Alternatively, if you already have a Hilt fragment instance available in your test, you can get any FragmentComponent binding using an EntryPoint .

Warning:Hilt does not currently support FragmentScenario because there is no way to specify an activity class, and Hilt requires a Hilt fragment to be contained in a Hilt activity. One workaround for this is to launch a Hilt activity and then attach your fragment.

Replacing bindings

It’s often useful for tests to be able to replace a production binding with a fake or mock binding to make tests more hermetic or easier to control in test. The next sections describe some ways to accomplish this in Hilt.

@TestInstallIn

A Dagger module annotated with @TestInstallIn allows users to replace an existing @InstallIn module for all tests in a given source set. For example, suppose we want to replace ProdDataServiceModule with FakeDataServiceModule . We can accomplish this by annotating FakeDataServiceModule with @TestInstallIn , as shown below:

A @TestInstallIn module can be included in the same source set as your test sources, as shown below:

However, if a particular @TestInstallIn module is needed in multiple Gradle modules, we recommend putting it in its own Gradle module (usually the same one as the fake), as shown below:

Putting the @TestInstallIn in the same Gradle module as the fake has a number of benefits. First, it ensures that all clients that depend on the fake properly replace the production module with the test module. It also avoids duplicating FakeDataServiceModule for every Gradle module that needs it.

Note that @TestInstallIn applies to all tests in a given source set. For cases where an individual test needs to replace a binding that is specific to the given test, the test can either be moved into its own source set, or it can use Hilt testing features such as @UninstallModules , @BindValue , and nested @InstallIn modules to replace bindings specific to that test. These features will be described in more detail in the following sections.

@UninstallModules

Warning:Test classes that use @UninstallModules , @BindValue , or nested @InstallIn modules result in a custom component being generated for that test. While this may be fine in most cases, it does have an impact on build speed. The recommended approach is to use @TestInstallIn modules instead.

A test annotated with @UninstallModules can uninstall production @InstallIn modules for that particular test (unlike @TestInstallIn , it has no effect on other tests). Once a module is uninstalled, the test can install new, test-specific bindings for that particular test.

There are two ways to install a new binding for a particular test:

  • Add an @InstallIn module nested within the test that provides the binding.
  • Add an @BindValue field within the test that provides the binding.
Читайте также:  Android zerolte что это

These two approaches are described in more detail in the next sections.

Note: @UninstallModules can only uninstall @InstallIn modules, not @TestInstallIn modules. If a @TestInstallIn module needs to be uninstalled the module must be split into two separate modules: a @TestInstallIn module that replaces the production module with no bindings (i.e. only removes the production module), and a @InstallIn module that provides the standard fake so that @UninstallModules can uninstall the provided fake.

Nested @InstallIn modules

Warning:Test classes that use @UninstallModules , @BindValue , or nested @InstallIn modules result in a custom component being generated for that test. While this may be fine in most cases, it does have an impact on build speed. The recommended approach is to use @TestInstallIn modules instead.

Normally, @InstallIn modules are installed in the Hilt components of every test. However, if a binding needs to be installed only in a particular test, that can be accomplished by nesting the @InstallIn module within the test class.

Thus, if there is another test that needs to provision the same binding with a different implementation, it can do that without a duplicate binding conflict.

In addition to static nested @InstallIn modules, Hilt also supports inner (non-static) @InstallIn modules within tests. Using an inner module allows the @Provides methods to reference members of the test instance.

Note: Hilt does not support @InstallIn modules with constructor parameters.

@BindValue

Warning:Test classes that use @UninstallModules , @BindValue , or nested @InstallIn modules result in a custom component being generated for that test. While this may be fine in most cases, it does have an impact on build speed. The recommended approach is to use @TestInstallIn modules instead.

For simple bindings, especially those that need to also be accessed in the test methods, Hilt provides a convenience annotation to avoid the boilerplate of creating a module and method normally required to provision a binding.

@BindValue is an annotation that allows you to easily bind fields in your test into the Dagger graph. To use it, just annotate a field with @BindValue and it will be bound to the declared field type with any qualifiers that are present on the field.

Note that @BindValue does not support the use of scope annotations since the binding’s scope is tied to the field and controlled by the test. The field’s value is queried whenever it is requested, so it can be mutated as necessary for your test. If you want the binding to be effectively singleton, just ensure that the field is only set once per test case, e.g. by setting the field’s value from either the field’s initializer or from within an @Before method of the test.

Similarly, Hilt also has a convenience annotation for multibindings with @BindValueIntoSet , @BindElementsIntoSet , and @BindValueIntoMap to support @IntoSet , @ElementsIntoSet , and @IntoMap respectively. (Note that @BindValueIntoMap requires the field to also be annotated with a map key annotation.)

Warning:Be careful when using @BindValue or non-static inner modules with ActivityScenarioRule . ActivityScenarioRule creates the activity before calling the @Before method, so if an @BindValue field is initialized in @Before (or later), then it’s possible for the Activity to inject the binding in its unitialized state. To avoid this, try initializing the @BindValue field in the field’s initializer.

Custom test application

Every Hilt test must use a Hilt test application as the Android application class. Hilt comes with a default test application, HiltTestApplication , which extends MultiDexApplication ; however, there are cases where a test may need to use a different base class.

@CustomTestApplication

If your test requires a custom base class, @CustomTestApplication can be used to generate a Hilt test application that extends the given base class.

To use @CustomTestApplication , just annotate a class or interface with @CustomTestApplication and specify the base class in the annotation value:

In the above example, Hilt will generate an application named MyCustom_Application that extends MyBaseApplication . In general, the name of the generated application will be the name of the annotated class appended with _Application . If the annotated class is a nested class, the name will also include the name of the outer class separated by an underscore. Note that the class that is annotated is irrelevant, other than for the name of the generated application.

Best practices

As a best practice, avoid using @CustomTestApplication and instead use HiltTestApplication in your tests. In general, having your Activity, Fragment, etc. be independent of the parent they are contained in makes it easier to compose and reuse it in the future.

However, if you must use a custom base application, there are some subtle differences with the production lifecycle to be aware of.

One difference is that instrumentation tests use the same application instance for every test and test case. Thus, it’s easy to accidentally leak state across test cases when using a custom test application. Instead, it’s better to avoid storing any test or test case dependendent state in your application.

Another difference is that the Hilt component in a test application is not created in super#onCreate . This restriction is mainly due to fact that some of Hilt’s features (e.g. @BindValue ) rely on the test instance, which is not available in tests until after Application#onCreate is called. Thus, unlike production applications, custom base applications must avoid calling into the component during Application#onCreate . This includes injecting memebers into the application. To prevent this issue, Hilt doesn’t allow injection in the base application.

Hilt rule order

If your test uses multiple test rules, make sure that the HiltAndroidRule runs before any other test rules that require access to the Hilt component. For example ActivityScenarioRule calls Activity#onCreate , which (for Hilt activities) requires the Hilt component to perform injection. Thus, the ActivityScenarioRule should run after the HiltAndroidRule to ensure that the component has been properly initialized.

Источник

Оцените статью