Testing practices based on Dagger Hilt

This document about Dagger Hilt, a DI library recommended by Google for Android development (available in the JVM language). https://dagger.dev/hilt/testing-philosophy There was a concept that I personally stabbed about a pretty good test, so I will write it down. Also, I feel that you can see the good points of Hilt other than the simple part.


It describes testing practices based on Dagger Hilt. Dagger Hilt's APIs and features are built on the implicit philosophy of what makes good tests. However, good testing is not accepted by everyone, so it is a document to clarify the philosophy of testing in the Hilt team.

What to test

Hilt recommends external "** testing from the user's perspective **". External users have a lot of meaning. It refers to real users, as well as users of classes and APIs. The important thing is "don't express the details of the implementation". Writing tests that depend on internal implementations, such as tests that depend on internal methods, makes the tests fragile. ** If the internal method is renamed, a good test doesn't need to change anything. The only factor that breaks the current test is when there are changes visible to the user. ** **

Take advantage of actual dependencies

** Hilt's testing philosophy does not force every class to write a test individually. In reality, such rules violate the principle of "testing from the user's perspective". ** Keep the test as small as necessary to make it easier to write and run. (Fast, does not consume a lot of resources, etc.) If there is no other difference, ** the test prioritizes the following in this order **

However, there are trade-offs to this.

Hilt solves the first problem. (Details below) Performance can be an issue, but in most cases it's not. It can only be a problem if there are I / O dependencies. If it is convenient and robust to take advantage of real dependencies without significantly compromising performance, then real dependencies should be used. If this has a significant negative impact on the test, Hilt provides a way to replace the binding.

Great advantage of using more real dependencies

If you can't use real dependencies, then use the standard Fake provided by the library. Standard Fake is better than Mock if it is maintained by the author of the library or by being provided with robust coverage. For these reasons ** Mock is a last resort **.

Hilt, DI, and Test

With these foundations, we enter "Hilt, DI, and Test". ** Dagger Hilt's answer to "use real dependencies" is to use DI / Dagger in Test **. This is closer to reality, because the objects are created to be done in production. This means that tests are less fragile than production code, making real objects easier to use. In fact, if you have a @ Inject constructor, it's easier and less code to use Dagger than to create a Mock.

Unfortunately, doing such a test without Hilt has been difficult until now due to the work of setting up the boilerplate and Dagger. But Hilt can generate boilerplate code and set up different settings for testing when Fake or Mock is needed. With Hilt, this problem doesn't prevent you from writing tests in Dagger, and you can easily take advantage of the actual dependencies.

Here's how Hilt actually replaces dependencies with Test. https://qiita.com/takahirom/items/3231edf2a430569b3e9d#testing

Disadvantages of other solutions

The method of not using Dagger in unit tests is a very common method. This unfortunately has a major drawback, but it is understandable given the difficulty of using Dagger without Hilt. For example, try to test the Foo class.

class Foo @Inject constructor(bar: Bar) {
}

If you don't use Dagger in this case, just call the constructor. At first glance this is very simple and understandable, but it starts to collapse when you start adapting Bar to Foo's constructor.

Direct instantiation in tests facilitates the use of Mock

You should use the real Bar class as much as possible by "using real dependencies" that we talked about earlier. But what should I do? To use the Foo class in the test, this is actually a recurrence: you have to instantiate it yourself, so you also need to instantiate the ** Bar, as well if you have a dependency on the Bar You need to instantiate them. To avoid getting too deep, you should start using Fake and Mock, not because of test speed or performance, but because many fragile boilerplate code causes maintenance problems. This is not a good reason to start using Fake or Mock. And now I'm forced to do this. ** ** As discussed earlier, the standard Fake method can reduce the maintenance burden of direct instantiation. But it's not always simple. (Omitted: Similarly, FakeBar depends on Clock, etc. Fake also has to manage dependencies.)

This usually encourages developers to use Mock. ** Mock solves the problem of chaining dependencies, but has the serious drawback of being quietly left behind or making testing useless for the overall purpose of finding real bugs. ** Since no one but the test author checks the behavior of the Mock, it is quite possible that the test will not test useful scenarios over time.

Direct instantiation in tests represents implementation details

** Direct instantiation breaks the practice of "don't represent implementation details". Because to call the constructor of the dependency details **. If the Bar has @Inject construcotr, you don't need to know that it depends on the Bar as Foo may refactor the implementation details. To explain this point, when Foo has a dependency such as Bar or Baz like Foo (Bar, Baz), nothing happens in Dagger even if the order of this parameter is changed. However, if you were instantiating directly, you would need to change the test. Similarly, adding a new @Inject class or optional binding does not need to be changed in production, but it will need to be changed in testing.

Summary

Hilt is designed to cure the disadvantages of using Dagger in testing, so that you can easily write tests using real dependencies. Testing with Hilt is generally a good experience if you follow these philosophies.

Recommended Posts

Testing practices based on Dagger Hilt
Dagger Hilt (DevFest 2020 document)