preface

In computer programming, unit testing is a method of software testing by which individual unit functions of source code are tested for suitability for use. Writing unit tests for code has many benefits, including early detection of code errors, facilitating change, simplifying integration, facilitating code refactoring, and many other features. Those of you who use the Java language have probably used or heard that Junit is used for unit testing, so why do we need Mockito? Imagine such a common scenario, the current classes to test depends on some other object, if use Junit for unit testing, we will have to manually create these dependent objects, it is actually a more troublesome work, at this point you can use Mockito test framework to simulate those who rely on class, These simulated objects act as virtual objects or clones of real objects during testing, and Mockito also provides convenient verification of test behavior. This allows us to focus more on the logic of the current test class than on the object it depends on.

Mock object generation method

To use Mockito, we first need to introduce the Mockito test framework dependencies in our project. For projects built on Maven, we need to introduce the following dependencies:

< the dependency > < groupId > org. Mockito < / groupId > < artifactId > mockito - core < / artifactId > < version > 3.3.3 < / version > <scope>test</scope> </dependency>Copy the code

If a project is built on Gradle, the following dependencies are introduced:

testCompile group: 'org.mockito', name: 'mockito-core', version: '3.3.3'
Copy the code

There are two common ways to create Mock objects using Mockito.

Mockito. Mock (clazz

Mock objects are created using the mock static method of the Mockito class. For example, a mock object of type List is created:

List<String> mockList = Mockito.mock(ArrayList.class);
Copy the code

Since a mock method is static, it is usually written as a statically imported method, that is, List

mockList = mock(arrayList.class).

2. Use @mock annotations

The second way is to create Mock objects using the @mock annotation method, And the need to pay attention to use the way is to use MockitoAnnotations before the run test method. The initMocks (this) or unit test class with @ ExtendWith MockitoExtension. Class notes, The following code creates a Mock object of type List (PS: @beforeeach is a Junit 5 annotation that functions like Junit 4’s @before annotation.) :

/ * * *@author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
//@ExtendWith(MockitoExtension.class)
public class MockitoTest {

  @Mock
  private List<String> mockList;

  @BeforeEach
  public void beforeEach(a) {
    MockitoAnnotations.initMocks(this); }}Copy the code

Validation test

The Mockito test framework provides static method Mockito. Verify so that we can easily carry out verification tests, such as method call verification, method call times verification, method call sequence verification, etc.

Verify method calls once

Verify that the size method of the mockList object is called once. The size method of the mockList object is called once.

/ * * *@author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_SimpleInvocationOnMock(a) { mockList.size(); verify(mockList).size(); }}Copy the code
Validates method calls for the specified number of times

Verify (atLeast + atMost); verify (atLeast + atMost); verify (atLeast + atMost); There are also methods such as never that verify that they are not called.

/ * * *@author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_NumberOfInteractionsWithMock(a) {
    mockList.size();
    mockList.size();

    verify(mockList, times(2)).size();
    verify(mockList, atLeast(1)).size();
    verify(mockList, atMost(10)).size(); }}Copy the code
Verify method call order

You can also use the inOrder method to verify the order in which methods are called, and the following example verifies the order in which the mockList object’s size, Add, and clear methods are called.

/ * * *@author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_OrderedInvocationsOnMock(a) {
    mockList.size();
    mockList.add("add a parameter");
    mockList.clear();

    InOrder inOrder = inOrder(mockList);

    inOrder.verify(mockList).size();
    inOrder.verify(mockList).add("add a parameter"); inOrder.verify(mockList).clear(); }}Copy the code

These are just a few simple validation tests, but validation test method call timeouts and more validation tests can be explored in the official documentation.

Abnormal verification method

Exception testing We need to use some of the call behavior definitions provided by the Mockito framework. Mockito provides when(…) .thenXXX(…) To let us define the method call behavior, the following code defines that when the mockMap GET method is called it throws a NullPointerException regardless of any parameters passed in, and then validates the call with Assertions. AssertThrows.

/ * * *@author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
@ExtendWith(MockitoExtension.class)
public class MockitoExceptionTest {

  @Mock
  public Map<String, Integer> mockMap;

  @Test
  public void whenConfigNonVoidReturnMethodToThrowEx_thenExIsThrown(a) {
    when(mockMap.get(anyString())).thenThrow(NullPointerException.class);

    assertThrows(NullPointerException.class, () -> mockMap.get("mghio")); }}Copy the code

At the same time the when (…). .thenXXX(…) You can define not only the exception thrown by a method call, but also the result returned after the call, such as when(mockMap.get(“mghio”)).thenReturn(21); Defines that 21 is returned when we call the mockMap get method and pass in the parameter mghio. It is important to note that the mock object test defined in this way does not actually affect the internal state of the object, as shown below:

Although we have called the Add method on the mockList object, mgHIO is not actually added to the mockList collection, so if we want to affect mock objects, we need to use the Spy method to generate mock objects.

public class MockitoTest {

  private List<String> mockList = spy(ArrayList.class);

  @Test
  public void add_spyMockList_thenAffect(a) {
    mockList.add("mghio");

    assertEquals(0, mockList.size()); }}Copy the code

After the breakpoint, you can see that mghiO is successfully added to the mockList collection after the add method is called from the mock object created using the Spy method.

Integration with the Spring framework

The Mockito framework provides the @MockBean annotation to inject a mock object into the Spring container that replaces any existing beans of the same type in the container. This annotation is useful in test scenarios where you need to emulate a particular bean, such as an external service. If you are using Spring Boot 2.0+ and already have the same type of bean in the container, You need to set Spring.main. allow-bean-definition-overriding to true (the default is false) to allow beans to define override. Let’s assume that to test querying user information by user encoding, we have a UserRepository of the database manipulation layer, the object we mock later, defined as follows:

/ * * *@author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
@Repository
public interface UserRepository {

  User findUserById(Long id);

}
Copy the code

There is also a related service for user operations, the UserService class, defined as follows:

/ * * *@author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
@Service
public class UserService {

  private UserRepository userRepository;

  public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public User findUserById(Long id) {
    returnuserRepository.findUserById(id); }}Copy the code

Annotate the UserRepository property with @MockBean in the test class to indicate that the bean of this type uses mock objects, and use the @AutoWired annotation to indicate that the UserService property uses objects from the Spring container. Then use @springboottest to enable the Spring environment.

/ * * *@author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
@SpringBootTest
public class UserServiceUnitTest {

  @Autowired
  private UserService userService;

  @MockBean
  private UserRepository userRepository;

  @Test
  public void whenUserIdIsProvided_thenRetrievedNameIsCorrect(a) {
    User expectedUser = new User(9527L."mghio"."18288888880");
    when(userRepository.findUserById(9527L)).thenReturn(expectedUser);
    User actualUser = userService.findUserById(9527L); assertEquals(expectedUser, actualUser); }}Copy the code

How the Mockito framework works

Mockito Mockito Mockito Mockito Mockito Mockito Mockito Mockito Mockito Mockito Mockito Mockito Mockito Mockito Mockito Mockito

public class Target {

  public String foo(String name) {
    return String.format("Hello, %s", name); }}Copy the code

Then we use the mockito.mock method and when(…) directly. .thenReturn(…) To generate the mock object and specify the behavior of the method call as follows:

@Test
public void test_foo(a) {
  String expectedResult = "Mocked mghio";
  when(mockTarget.foo("mghio")).thenReturn(expectedResult);
  String actualResult = mockTarget.foo("mghio");
  assertEquals(expectedResult, actualResult);
}
Copy the code

When (mockTarget.foo(“mghio”)).thenReturn(expectedResult) is expectedResult. The input to the when method turns out to be mockTarget.foo(“mghio”), the correct code would be when(mockTarget).foo(“mghio”), but it doesn’t actually compile. Since the target. foo method returns a String, can we use the following approach?

Mockito.when("Hello, I am mghio").thenReturn("Mocked mghio");
Copy the code

The result is that the compiler passed, but an error was reported at runtime:

As you can see from the error message, the when method requires a method call parameter. In fact, it only needs the more object method to be called before the when method.

@Test
public void test_mockitoWhenMethod(a) {
  String expectedResult = "Mocked mghio";
  mockTarget.foo("mghio");
  when("Hello, I am mghio").thenReturn(expectedResult);
  String actualResult = mockTarget.foo("mghio");
  assertEquals(expectedResult, actualResult);
}
Copy the code

The above code can be tested normally and the results are as follows:

Why does this pass the normal test? Because when we call the mock object’s foo method, Mockito intercepts the method call and stores the details of the method call into the mock object’s context. When we call the mockito.when method, we actually get the last registered method call from that context. We then save the thenReturn parameter as its return value, and when we call the mock object’s method again, the previously recorded method behavior is replayed, which triggers the interceptor re-call and returns the return value we specified in thenReturn. The mockito.when method is mockito.when.

MockitoCore. When MockitoCore. When MockitoCore.

A closer look reveals that the source code does not use the methodCall parameter, but instead gets the OngoingStubbing object from the MockingProgress instance, which is the context object mentioned earlier. My impression is that Mockito created the “illusion” of when method calls in order to provide a concise and easy-to-use API. In short, the Mockito framework works by storing and retrieving method call details in context through method intercepts.

How do I implement a miniature Mock framework

Now that you know how Mockito works, let’s see how you can implement a mock framework with similar functionality yourself. However, a reading of the source code reveals that Mockito does not actually use the familiar method intercepts of Spring AOP or AspectJ. Instead, Mockito generates and initializes mock objects through the runtime enhancement library Byte Buddy and reflection library Objenesis. Now, with the above analysis and source code reading, you can define a simple version of the mock framework. Name your custom mock framework iMock. One thing to note here is that Mockito has the advantage that it does not require initialization and can be used immediately through the static methods it provides. Here we also use a static method with the same name, using the Mockito source code:

It is easy to see that Mockito classes are ultimately delegated to MockitoCore to implement functions, which only provide some user-friendly static methods. Here we also define a proxy object like mockCore. This class requires a mock method that creates mock objects and a thenReturn method that sets the method’s return value. It also holds a list of method call details called InvocationDetail. This class is used to record method call details. The when method then returns only the last InvocationDetail in the list, which can be done using a common Java ArrayList, The ArrayList collection list here implements the functionality of OngoingStubbing in Mockito. It is easy to code the InvocationDetail class based on the method’s three elements: the method name, method parameters, and method return value. To distinguish between methods that have the same name in different classes, We also need to add the class’s full name field and override the equals and hashCode methods of the class (to determine whether we need to use them when calling the method collection list), like this:

/ * * *@author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
public class InvocationDetail<T> {

  private String attachedClassName;

  private String methodName;

  private Object[] arguments;

  private T result;

  public InvocationDetail(String attachedClassName, String methodName, Object[] arguments) {
    this.attachedClassName = attachedClassName;
    this.methodName = methodName;
    this.arguments = arguments;
  }

  public void thenReturn(T t) {
    this.result = t;
  }

  public T getResult(a) {
    return result;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null|| getClass() ! = o.getClass())return false; InvocationDetail<? > behaviour = (InvocationDetail<? >) o;return Objects.equals(attachedClassName, behaviour.attachedClassName) &&
        Objects.equals(methodName, behaviour.methodName) &&
        Arrays.equals(arguments, behaviour.arguments);
  }

  @Override
  public int hashCode(a) {
    int result = Objects.hash(attachedClassName, methodName);
    result = 31 * result + Arrays.hashCode(arguments);
    returnresult; }}Copy the code

Here we also use the Byte Buddy and Objenesis libraries to create mock objects. The IMockCreator interface is defined as follows:

/ * * *@author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
public interface IMockCreator {

  <T> T createMock(Class<T> mockTargetClass, List<InvocationDetail> behaviorList);

}
Copy the code

The implementation class ByteBuddyIMockCreator uses the ByteBuddy library to dynamically generate mock object code at run time and then instantiates the object using Objenesis. The code is as follows:

/ * * *@author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
public class ByteBuddyIMockCreator implements IMockCreator {

  private final ObjenesisStd objenesisStd = new ObjenesisStd();

  @Override
  public <T> T createMock(Class<T> mockTargetClass, List<InvocationDetail> behaviorList) {
    ByteBuddy byteBuddy = new ByteBuddy();

    Class<? extends T> classWithInterceptor = byteBuddy.subclass(mockTargetClass)
        .method(ElementMatchers.any())
        .intercept(MethodDelegation.to(InterceptorDelegate.class))
        .defineField("interceptor", IMockInterceptor.class, Modifier.PRIVATE)
        .implement(IMockIntercepable.class)
        .intercept(FieldAccessor.ofBeanProperty())
        .make()
        .load(getClass().getClassLoader(), Default.WRAPPER).getLoaded();

    T mockTargetInstance = objenesisStd.newInstance(classWithInterceptor);
    ((IMockIntercepable) mockTargetInstance).setInterceptor(new IMockInterceptor(behaviorList));

    returnmockTargetInstance; }}Copy the code

Based on the above analysis, we can easily write code for the IMockCore class that creates a mock object as follows:

/ * * *@author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
public class IMockCore {

  private final List<InvocationDetail> invocationDetailList = new ArrayList<>(8);

  private final IMockCreator mockCreator = new ByteBuddyIMockCreator();

  public <T> T mock(Class<T> mockTargetClass) {
    T result = mockCreator.createMock(mockTargetClass, invocationDetailList);
    return result;
  }

  @SuppressWarnings("unchecked")
  public <T> InvocationDetail<T> when(T methodCall) {
    int currentSize = invocationDetailList.size();
    return (InvocationDetail<T>) invocationDetailList.get(currentSize - 1); }}Copy the code

The IMock class provided to the consumer is just a simple call to IMockCore as follows:

/ * * *@author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
public class IMock {

  private static final IMockCore IMOCK_CORE = new IMockCore();

  public static <T> T mock(Class<T> clazz) {
    return IMOCK_CORE.mock(clazz);
  }

  public static <T> InvocationDetail when(T methodCall) {
    returnIMOCK_CORE.when(methodCall); }}Copy the code

Now that we’ve implemented a miniature mock framework, let’s test it out with a practical example. First create a Target object:

/ * * *@author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
public class Target {

  public String foo(String name) {
    return String.format("Hello, %s", name); }}Copy the code

Then write the corresponding test class IMockTest as follows:

/ * * *@author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @sinceJDK 1.8 * /
public class IMockTest {

  @Test
  public void test_foo_method(a) {
    String exceptedResult = "Mocked mghio";
    Target mockTarget = IMock.mock(Target.class);

    IMock.when(mockTarget.foo("mghio")).thenReturn(exceptedResult);

    String actualResult = mockTarget.foo("mghio"); assertEquals(exceptedResult, actualResult); }}Copy the code

The above tests can run normally and achieve the same effect as the Mockito test framework. The running results are as follows:

All the code of the custom IMock framework has been uploaded to the Github repository IMock, interested friends can go to have a look.

conclusion

This article only introduces some usage methods of Mockito. This is only the most basic function provided by the framework. For more advanced usage, you can go to the official website to read related documentation. .thenReturn(…) Imock defines how behavior methods are implemented and implements a simplified version of the same functionality along the lines of its source code. Although unit testing has many advantages, it should not be carried out blindly. In most cases, it is ok to do unit testing on the core business modules in the project that are logically complex and not easy to understand, as well as the modules that are commonly relied on in the project.


Refer to the article

Mockito

Objenesis

Byte Buddy