In the last article, we explained that to make a mock dependency work, you need to somehow set it into the object in a test environment, replacing the real implementation. The example we used earlier is:

public class LoginPresenter { private UserManager mUserManager = new UserManager(); Public void login(String username, String password) {//... some other code mUserManager.performLogin(username, password); }}Copy the code

When testing LoginPresenter#login(), in order to be able to mock out the UserManager set to LoginPresenter, we used the simple and crude method. Add a setter for UserManager to LoginPresenter. However, it’s not very elegant, and in general, we don’t call this setter in our formal code to modify the UserManager object. So the purpose of this setter is purely to facilitate testing. It’s not unnecessary, but it’s not pretty, so if we have a choice, we don’t do it. Here, we introduce dependency injection as a pattern.

An accurate definition of Dependency Injection (DI) can be found here. Here is a brief description of its basic concept. First, it is a code pattern, which has two concepts: Client and Dependency. If a class in your code uses another class, the former is called Client and the latter is called Dependency. LoginPresenter is called Client and UserManager is called Dependency. Of course, this is a relative concept; a class can be a Dependency of one class but a Client of another. For example, if Retrofit is used in UserManager, UserManager is Dependency as opposed to Retrofit. The basic idea of DI is that Dependency creation is not carried out in the Client, but is created externally and then set to the Client in some way. This pattern is called dependency injection.

Yes, dependency injection is such a simple concept, but it should be clarified that the concept itself has nothing to do with dagger2, RoboGuice or other frameworks. A lot of DI articles are written with Dagger2, and dagger2 is relatively unintuitive to use, leading many people to think that DI is complicated and that you can only implement dependency injection with a dagger framework, which is not the case. Implementing dependency injection is very simple, and the Dagger framework just makes it much simpler, concise and elegant.

A common implementation of DI

Here’s how DI is implemented, and in general, this is where Dagger2 gets a lot of attention. However, while Dagger2 is a really nice thing to do, if I were to introduce Dagger2 directly, it would be easy to make the mistake of thinking that I can only use dagger to do dependency injection or create the corresponding test class when testing, so I purposely don’t cover dagger here. Let people know how to implement the basic DI first, and then how to use it more efficiently when testing.

There are many ways to implement DI. The setter mentioned in the previous article is actually one way to implement DI. It is called setter Injection. An argument injection into a method is an injection of DI.

Public class LoginPresenter {// Here, LoginPresenter no longer holds a reference to UserManager, Public void login(UserManager UserManager, String username, String password) {//... some other code userManager.performLogin(username, password); }}Copy the code

More commonly, however, Dependency is passed as a parameter to the Client constructor:

public class LoginPresenter { private final UserManager mUserManager; Public LoginPresenter(UserManager UserManager) {this.muserManager = UserManager; } public void login(String username, String password) { //... some other code mUserManager.performLogin(username, password); }}Copy the code

This pattern for implementing DI is called Constructor Injection. In fact, in general, when I talk about DI, I mean this way. The advantage of this approach is that the dependencies are very obvious. You must provide the necessary dependency when creating this class. This is, in a way, an illustration of what this class does. Therefore, use Constructor injection whenever possible.

At this point, you might wonder if declaring dependencies in Constructor arguments would make the class too many Constructor arguments. When this happens, it is often a sign that the design of the class is flawed and needs refactoring. Why is that? There are two classes in our code, one is Data, UserInfo, OrderInfo, and so on. The other is Service classes, such as UserManager, AudioPlayer, and so on. So there are two cases of this problem:

  1. If much of what is passed in from Constructor is primitive data or data classes, then perhaps you need to create a data class (or another) to encapsulate this data. This process can be extremely valuable. Instead of just encapsulating arguments, you have a class, and a lot of methods can be put inside that class. For this point, please refer to Chapter 10 “Introduction Parameter Object” in Reconstruction by Martin Fowler.
  2. If many of the incoming service classes are service classes, then that class does too many things and does not comply with the Single Responsibility Principle (SRP), and therefore needs to be refactored.

Back to our original intention: the use of DI in testing.

The application of DI in unit tests

The application of DI in unit tests is to mock out a Dependency set to a Client using DI. To emphasize that we should use Constructor injection as much as possible, we will not do code examples for setter injection and Argument injection. If your code uses Constructor injection:

public class LoginPresenter { private final UserManager mUserManager; Public LoginPresenter(UserManager UserManager) {this.muserManager = UserManager; } public void login(String username, String password) { //... some other code mUserManager.performLogin(username, password); }}Copy the code

The method we want to test is login(), and verify that the login() method calls performLigon() of the mUserManager. The corresponding test method is as follows:

public class LoginPresenterTest { @Test public void testLogin() { UserManager mockUserManager = Mockito.mock(UserManager.class); LoginPresenter presenter = new LoginPresenter(mockUserManager); // Mock the presenter. Login ("xiaochuang", "xiaochuang password"); Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password"); }}Copy the code

Easy, right.

summary

This article introduces the concept of DI and how it can be used in unit testing. The purpose of this article is not to introduce the use of Dagger2, but to emphasize:

  1. The key to a flexible, easy-to-test, SRP-compliant, well-structured project is to apply the dependency injection pattern, not to do it.
  2. Once you have learned to use the dagger, remember that when testing, if you can mock dependency directly and pass it to the class under test, you do not have to use the dagger to do DI

However, if you don’t use the framework to do DI at all, there is a problem in the formal code where the dependency creation is left to the upper-level client, which is not a good thing. If you think about it, when you create a LoginPresenter in a LoginActivity, you also need to know that LoginPresenter uses UserManager. Then create a UserManager object for LoginPresenter. For LoginActivity, it says, I don’t care what UserManager you use, I just want to tell you login, you just give me the honest login, I don’t care what Manager you use. Therefore, creating the UserManager directly inside the LoginActivity may not be a good choice. So what’s a good choice? Dagger2 gives us the answer. So in the next article, we’ll introduce Dagger2.

The code in this article is in the github project.

Finally, if you are interested in Android unit testing, welcome to join our communication group, because there are more than 100 members of the group, we can not scan the code to join, please pay attention to the public account below to get the method of joining.