SpringBoot2.3. Integrate Mockito to realize unit test

Posted by AudiS2 on Wed, 05 Jan 2022 17:10:39 +0100

1. General

Mockito is an excellent and powerful framework for Java unit testing. When you need to call a third-party interface and the development test environment cannot call this interface directly, you can use mockito to simulate interface calls to write perfect unit tests, which also makes it strongly decoupled from third-party applications. For more details, please refer to Mockito official website

2. Introduce Mockito dependency

Because SpringBoot integrates Mockito itself, you only need to introduce test dependency when integrating Mockito to write unit tests

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>

In addition, there are some differences between the annotations used by Mockito in junit4 and junit5. If junit4 is used for unit testing, junit4 dependencies need to be introduced

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>

3. Interface code writing

This article uses spring boot 2 3.12 and mybatis plus3 4.2 write Dao layer and Service layer interfaces, which are not explained in detail here. For small partners who do not know how to use them, please refer to SpringBoot2.3.4 integrate mybatis plus3 4.0 and swagger3 0 In addition, mapstruct will be used. Please refer to SpringBoot2.3. Integrate MapStruct to realize Java bean mapping

4. Write test class

4.1. Introducing mock into test class

There are two methods to introduce Mock into the test class. One is to import the static method Mock into the code, and the other is to use the annotation @ Mock. The official website recommends using annotation to introduce Mock. Its advantages are as follows:

  1. Minimize duplicate simulation creation code
  2. Make test classes more readable
  3. Makes validation errors easier to read because field names are used to identify simulations

Note: when using @ Mock annotation, you need to add a runner to make the annotation take effect
junit4 has three methods, as follows:

  1. Initialize mock: mockitoannotations initMocks(this)
  2. Use the annotation MockitoJUnitRunner on the test class
  3. Using MockitoRule

The example code is as follows:

@Before
public void setUp() {
    MockitoAnnotations.initMocks(this);
}
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class SysUserInfoServiceJunitRunnerTest {}
@Rule
public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);

There are three ways for mockitojunit runner

  1. Silent: the implementation ignores stub parameter mismatch (MockitoJUnitRunner.StrictStubs) and does not detect unused stubs
  2. Strict: detects unused stubs and reports them as failures
  3. Strict stubs: improve debugging and testing to help keep testing clean

MockitoRule has two ways

  1. silent(): the rule does not report stub warnings during test execution
  2. Strictness (strictness): the strictness level, especially the "strictness. Strictness_stubs", helps to debug and keep the test clean. In addition, there are the strictness levels LENIENT and WARN

junit5 also has three methods, as follows:

  1. Initialize mock: mockitoannotations initMocks(this)
  2. Use the annotation @ ExtendWith(MockitoExtension.class) on the test class
  3. The @ MockitoSettings annotation is used on the test class to configure the severity level used for the test class

The example code is as follows:

@BeforeEach
void setUp() {
    MockitoAnnotations.initMocks(this);
}
@ExtendWith(MockitoExtension.class)
public class SysUserInfoServiceExtensionTest {}
@MockitoSettings(strictness = Strictness.STRICT_STUBS)
public class SysUserInfoServiceSettingsTest {}

4.2. Common notes

The annotations created by mock are
@Mock: used to create and inject simulation instances
@Spy: monitor existing instances
@InjectMocks: automatically inject simulated fields into the test object
@Captor: used to get parameters
@The difference between Mock and @ Spy

  • When @ mock is used to create a mock, it is created from the class of the type, not from the actual instance. Mock only creates a basic shell instance of the class to track its interaction.
  • When @ Spy is used, an existing instance will be wrapped and detected to track all interactions with it, except that it behaves the same as a normal instance.
  • Generally, @ Mock is used to access the third-party service interface, @ Spy is used to access the service interface (read the configuration class or mapstruct interface)

The example code is as follows:

class SysUserInfoServiceSpyTest {

    @Mock
    private SysUserInfoMapper userInfoMapper;
    @Spy
    private final UserInfoMapper infoMapper = new UserInfoMapperImpl();
    @InjectMocks
    private SysUserInfoServiceImpl userInfoService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.initMocks(this);
    }
}

4.3. Stubs (stub / piling)

Use when () Thenreturn() stub has an interface with a return value. Use when() Thenthrow() stub has an abnormal interface. Use when() Thenanswer() stub has the interface of callback function, using given() Willreturn() stub has a return value interface in BDD format. The first () is the method call of mock object, and the second () is the returned object.
The functions of both are the same. given() is the style format of behavior driven development (BDD).
The example code is as follows:

@Test
void getUserInfoByIdTest() {
    final SysUserInfo userInfo = SysUserInfo.builder()
            .id(1L)
            .userName("admin")
            .password("123456")
            .sex(2)
            .age(99)
            .email("admin@163.com")
            .createUser("admin")
            .createTime(LocalDateTime.now())
            .updateUser("admin")
            .updateTime(LocalDateTime.now())
            .build();
    Mockito.when(userInfoMapper.selectById(any())).thenReturn(userInfo);
    // perhaps
    // BDDMockito.given(userInfoMapper.selectById(any())).willReturn(userInfo);
    final SysUserInfo info = userInfoService.getById(1);
    Assertions.assertNotNull(info);
}

Use the method with do when

  • Stub void method
  • Stub method on spy object
  • Multiple stubs use the same method to change the behavior of the simulation during testing

Common methods with do include doThrow(), doAnswer(), doNothing(), doReturn(), and doCallRealMethod()
doReturn(): used to cause side effects when monitoring real objects and calling real methods in spy, or to overwrite previous exception stubs
doThrow(): used to throw an exception to the void method stub
doAnswer(): used to stub the void method. Callback value is required
Donoting(): used to stub the void method. Nothing needs to be done. The usage is to stub successive calls to the void method or monitor real objects, and the void method does not perform any operations
doCallRealMethod(): used to call the method that is actually executed

4.4. verification

verify
Used to verify that certain behaviors have occurred at least once, for example:

verify(userInfoMapper).selectById(anyInt());

perhaps

verify(userInfoMapper, times(1)).selectById(anyInt());

Verify that certain behaviors occur at least once / exactly times / never used
atLeastOnce(): occurs at least once

verify(userInfoMapper, atLeastOnce()).selectById(anyInt()); 

atLeast(num): occurs at least num times

verify(userInfoMapper, atLeast(1)).selectById(anyInt());

atMostOnce(): occurs at most once

verify(userInfoMapper, atMostOnce()).selectById(anyInt());

atMost(num): occurs num times at most

verify(userInfoMapper, atMost(1)).selectById(anyInt());

never(): never happened

verify(userInfoMapper, never()).selectOne(any());

only(): check whether the method to be called is unique

verify(userInfoMapper, only()).selectById(anyInt());

timeout(): validation is triggered for a given time (milliseconds) and can be used to test asynchronous code

verify(userInfoMapper, timeout(100)).selectById(anyInt());
verify(userInfoMapper, timeout(100).times(1)).selectById(anyInt());

after(): trigger validation after a given time (milliseconds) to test asynchronous code

verify(userInfoMapper, after(100)).selectById(anyInt());
verify(userInfoMapper, after(100).times(1)).selectById(anyInt());

What is the difference between timeout() and after()

  • timeout() successfully exits immediately after passing the validation
  • after() waits a given time before starting validation

ArgumentCaptor
It is used to obtain request parameters for further assertion. It is usually used in combination with verify(). Its applicable conditions are as follows:

  • Custom parameter matchers cannot be reused
  • You only need to assert the parameter value to verify

When introducing ArgumentCaptor into the test class, you can use the annotation @ Captor or or

ArgumentCaptor<Class> argumentCaptor = ArgumentCaptor.forClass(Class.class);

The main methods are capture(), getValue(), getAllValues()
capture(): used to capture parameter values. This method must be used inside validation

verify(userInfoMapper, times(1)).insert(argumentCaptor.capture());

getValue(): returns the captured parameter value. If the validation method is called multiple times, only the latest captured value is returned

assertEquals("admin", argumentCaptor.getValue().getUserName());

getAllValues(): returns all captured parameter values, which are used to capture variable parameters or call the validation method multiple times. When the varargs method is called multiple times, it returns a consolidated list of all values from all calls
InOrder
It is used to verify mock objects in order. You can only verify the required mock objects

final InOrder inOrder = inOrder(userInfoMapper, infoMapper);
inOrder.verify(userInfoMapper).selectById(anyInt());
inOrder.verify(infoMapper).map(any());

calls(): allows non greedy verification in order

inOrder.verify(userInfoMapper, calls(1)).selectById(anyInt());

Different from times(1), if the method is called twice, no error will be reported
Unlike atLeast(1), the second time is not marked as verified

Topics: Java Spring Boot unit testing mockito