This is the 9th day of my participation in the November Gwen Challenge. See details: The Last Gwen Challenge 2021.

In the unit testing part of software development, external calls and third party code do not need to be tested because they may not be able to be tested and may affect the efficiency of testing. To avoid testing annoying external calls and third-party code, we often need to simulate the return values of these methods, which is a common mock technique for testing. JAVA testing framework Mockito is such a testing framework, this article will be simple Mockito working principle.

Mockito

Mockito is not difficult to use and easy to operate. But when asked about the specific working mechanism, I could not say it in one breath and needed to sort it out well.

The basic use

In use, invoke-when-then-invoke can be used according to the steps like invoke- invoke- invoke- invoke.

As shown in the following example, version 3.9.0 mockito-core was used:

< the dependency > < groupId > org. Mockito < / groupId > < artifactId > mockito - core < / artifactId > < version > 3.9.0 < / version > <scope>test</scope> </dependency>Copy the code
A a = Mockito.mock(A.class);
System.out.println("a.test() = " + a.test());
Mockito.when(a.test()).thenReturn(new MyMap());
System.out.println("a.test() = " + a.test());
Copy the code
  • Return as expected
a.test() = null
a.test() = {}
Copy the code
  • Pegging refers to a when-then process in which method parameters are specified and simulation results are returned.

What exactly did Mock do?

It looks simple enough to use, so what exactly does Mock do? How did you change the logic of the class?

  • Instead of modifying the logic of the original class, a mock object is generated by calling the mock tool function
  • This object is a subclass of the original class

The schematic diagram is as follows:

  • Subclasses are generated at run time using the bytecode framework
    • Bytecodes generated using Byte Buddy bytecode framework processing technology
    • Bytecode can be loaded directly, selecting an appropriate class loader to load the generated bytecode, and instantiating it as an object

Why generate bytecode? Because if you generate the source code, you have to compile and then load it, which is an extra step.

As you can see from the diagram, the methods handled by the mock object are handed over to the Handle method of the mockHandler instance. We’ll examine what this object and method do in more detail.

The diagram

In addition to generating bytecode, the mock also does something very important, generating instance objects as shown below:

These instances form part of mock and stub proceduresProcessors and containers:

  • MockHandler
  • invocationContainer
  • stubbed

MockHandler objects such as

The process shown above can be verified in code, and the main execution logic is in the createMock method

    public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
        // Generate and load the mock class
        Class<? extends T> mockedProxyType = createMockType(settings);
        / / instantiate
        T mockInstance = instantiator.newInstance(mockedProxyType);
        MockAccess mockAccess = (MockAccess) mockInstance;
        // Assign <-handler to the mockitoInterceptor member variable
        mockAccess.setMockitoInterceptor(new MockMethodInterceptor(handler, settings));
        return ensureMockIsAssignableToMockedType(settings, mockInstance);
    }
Copy the code
  • MockHandler MockHandler = createMockHandler(Settings)

  • Create invocationContainer inside the handler method: this.invocationContainer = new InvocationContainerImpl(mockSettings)

  • Stubbed LinkedList

    creates stubbed LinkedList

    in invocationContainer to store piling functions and arguments and return results.

  • Assign the object member variable mockitoInterceptor to handler

    Here, we have declared the mockitoInterceptor member using Bytebuddy generated bytecode. So again by mockAccess. SetMockitoInterceptor (new MockMethodInterceptor (handler, Settings)) is assigned to it.

 Bytebuddy.builder()...// Set the parent class and name
            .method(matcher)
            .intercept(dispatcher)
            ...// Set other method properties, such as synchronized Settings
            // Declare the member variable: mockitoInterceptor
            .defineField("mockitoInterceptor", MockMethodInterceptor.class, PRIVATE)
Copy the code

After that, the mockHander member of the mock object can be obtained from the getMockitoInterceptor function, which is important.

Diagram enhancement

In addition, MockingProgress is a utility class for stub procedures, and instances of the MockingProgress class are also generated during the mock process. You can see:

public <T> T mock(Class<T> typeToMock, MockSettings settings) {... T mock = createMock(creationSettings); mockingProgress().mockingStarted(mock, creationSettings);return mock;
}
Copy the code
  • mockingProgress()Is to generate aThreadLocal<MockingProgress>
  • ThreadLocal represents a thread variable and can support multithreaded concurrency.

Thus, the existing diagram is enhanced:

The role of MockingProgress is examined below.

Execute the process

Suppose thread Thread1 starts to mock, and the process looks like this:

  • A a=Mockito.mock(A.class)Initialize the…
  • Mockito.when(a.func(...) ).then..., which can be decomposed into the following three processes

call

Here’s the call from when:

  • A.f unc (" ABC ")To perform thehandlemethods
  • The handle method generates a local ongoingStubbing object
  • Handler passes its own member invocationContainer to ongoingStubbing
  • The handler pushes the generated ongoingStubbing object to mockingProgress
  • The default empty result is null

The following code looks like this:

    public Object handle(Invocation invocation) throws Throwable {
        OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl<T>(invocationContainer);
        mockingProgress().reportOngoingStubbing(ongoingStubbing);
        StubbedInvocationMatcher stubbing = invocationContainer.findAnswerFor(invocation);
        if(stubbing ! =null) {
            stubbing.captureArgumentsFrom(invocation);
            return stubbing.answer(invocation);
        } else {
            // The default return value provided by Mockito's original withSettings(), null,
            // The call in when is the default value returned
            returnmockSettings.getDefaultAnswer().answer(invocation); }}Copy the code

when

Execution of the when logic: Pull the pull object ongoingStubbing from the current thread’s mockingProgress

  • mockingProgress
    • ifstubbingInProgress! =null,In front of a stubIt’s not finished yetAn exception is thrown
    • ifstubbingInProgress=null, the settingstubbingInProgress, returns ongoingStubbing to when,Set your own ongoingStubbing to NULLAnd the when functionReturn ongoingStubbing
 public <T> OngoingStubbing<T> when(T methodCall) {
        mockingProgress().stubbingStarted();/ / if stubbingInProgress! =null, the stub is not complete
        return (OngoingStubbing<T>) mockingProgress.pullOngoingStubbing();
    }
Copy the code

then

  • The ongoingStubbing returned by the when function calls the then method.
  • thenthenGet it from ongoingStubbing member invocationContainerstubbed
  • Lock and modify the list, that is, add a piling function and argument and return the result.

As you can see from invocationContainer, the method argument sent by invoke must correspond to the return value set after it is fetched, otherwise the then execution will report an error

Call again

The result of the stub is finally used

  • a.func("abc")From:invocationContainerthestubbedMatches the call parameters, and returns the result of the call

MockingProgress bootstrap state evolution

For MockingProgress

  • invokeSend ongoingStubbing
    • You can send them as many times as you want
  • whenTaking ongoingStubbing
    • You can’t take it more than once

Whether ongoingStubbing is null is determined during each operation

  • When requires ongoingStubbing to have a value, and then is called by ongoingStubbing

  • UnfinishedStubbingException

    • Stub procedures when-then are atomic, and calls to a’s methods or continuing to issue new stubs without the stub completing are thrownMethod threw 'org.mockito.exceptions.misusing.UnfinishedStubbingException' exception.

MockingProgress explore again

Small experiment: Multi-threaded mock

When two threads mock at the same time, they do not affect each other because the stub procedure is ThreadLocal. One thread mocks data that another thread can use.

@Test
void multiMockA(a) throws Exception{
    A a = Mockito.mock(A.class);
    Mockito.when(a.func("one")).thenReturn(1);
    Thread thread = new Thread(() -> {
            Mockito.when(a.func("two")).thenReturn(2);
        System.out.println("a.func("one") =" + a.func("one"));
    });
    thread.start();
    thread.join();
    System.out.println("a.func("two") =" + a.func("two"));
}
Copy the code

return

a.func("one") = 1
a.func("two") = 2
Copy the code

The purpose of this small experiment is to try to understand, but not to advocate, this use.

Small experiment: Mock out multiple classes in one thread

The following code:

@Test
void mocMultiClass(a) throws Exception{
    A a = Mockito.mock(A.class);
    B b= Mockito.mock(B.class);
    Mockito.when(a.func("one")).thenReturn(1);
    Mockito.when(b.func(1)).thenReturn("one");
    System.out.println("a.func("one") =" + a.func("one"));
    System.out.println("b.func(1) = " + b.func(1));
}
Copy the code
a.func("one") = 1
b.func(1) = one
Copy the code

There is no problem with this normal operation. MockingProgress helps A stub and then B mock, as shown below:

Features that are not commonly used but can be understood

The most common pattern is the one described in detail above, but there are also some less common but understandable features, mainly the following two.

SPY

Spy is another way of staking. Compared to mocks, the interceptor handles different logic when generating bytecode.

  • The object generated by the Spy, the invoke method call before the Stub, is the method processing logic that calls the original class (super).
  • The mock generated object whose method call before the stub returns a null value, such as NULL, and the collection class is an empty collection.
    • Mockito supports null-value returns for several collection classes
    • For return types such as HashMap, Mockito returns an empty collection
    • But if the class returned is derived from HashMap, such as NutMap, Mockito still returns NULL

VERIFY mode

  • Verify that the MOCK method has been called
  • Verify the number of MOCK method calls and so on

experience

Summary of some production process encountered several cases to share, including the use of Mock in SpringBoot, Mock with generic objects encountered problems and some of the arguments of the stake.

SpringBoot and Mock

In the Spingboot test, we can use:

  • MockBean is used to inject a mock singleton object
  • @SpyBean is used to inject a spy singleton object

Mocks and generics

  • The experiment about
@Test
void mockGeneric(a){
    HashMap<String,Integer> list = Mockito.mock(HashMap.class);
    Mockito.when(list.get("one")).thenReturn(1);
    System.out.println("list.get("one") =" + list.get("one"));
}
Copy the code
list.get("one") = 1
Copy the code

And that seems perfectly ok.

But there is a problem with @mockBean and you have to use generics. because

@MockBean
KafkaTemplate<String, Object> kafkaTemplate;
Copy the code

and

@MockBean
KafkaTemplate<String, String> kafkaTemplate;
Copy the code

These two are different because KafkaTemplate

KafkaTemplate and KafkaTemplate

KafkaTemplate are also two different beans.
,>
,>

Of course this is not a Mockito problem, but a Spring Boot problem. You can verify that.

Parameter of pile insertion

  • There are two types of parameters: normal parameters and matchers
  • matcher: A series of functions can be used to match arguments, including mockito. anyArgumentMatchersClass for all static methods provided under theEq, startsWithAnd so on.
  • Method must use the same pattern for multiple arguments, otherwise it will be thrownInvalidUseOfMatchersException(Argument matching) exception, that is, if one argument is in match mode and the other cannot use fixed mode (provided by non-Argumentmatchers).