“This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!”

preface

In the previous Article, Spring Boot Unit Testing Practices, we explained that external dependencies in unit tests need to be mocked to ensure R (repeatable) for test cases.

How do you Mock operations that depend on MySQL, Redis, MQ, etc? This article uses Spring Boot 2.3, Junit5, and Mockito to demonstrate how to Mock and Stub, and includes some simple Junit5 operations.

MocK

The Mock method is a common technique in unit testing. Its main purpose is to simulate objects that are not easy to construct or complex in the application, thus separating the test from the objects outside the test boundary. 1

Stub

Stub/Method Stub is a program segment that replaces a function. Piles can be used to simulate the behavior of an existing program (such as a remote machine process) or as a temporary replacement for code to be developed. Therefore, piling technology is very useful in program migration, distributed computing, general software development and testing. 2

practice

Introduction of depend on

Spring Boot 2.3.12.RELEASE, JPA, RabbitMQ, Redis: update to Junit 5

Complete reliance on

Junit 5

Junit 5 is different from Junit 4, but not that much, and offers more full functionality in terms of assertions than Junit 4

Screenshot fromJUnit 5 vs. JUnit 4

In the case of Spring, the biggest difference is that to use the Spring container, you need to use the following:

@RunWith(SpringRunner.class)  => @ExtendWith(SpringExtension.class)
Copy the code

@RunWith(SpringRunner.class)It is possible but not possible to inject beans, including MockBean.

Prepare the environment

instructions

A business scenario is a phase of the prize-winning operation for an event poster, simplified and adapted from existing business logic

  • ActivityRepository: Active storage, operational data
  • ActivityService: Such dependenciesActivityRepositoryAs well asRedisTemplate
  • ActivityService#awardThis is a business method for unit testing, which relies on the database and Redis

Pseudocode to receive the award:

public void award(activityId, posterId, stageId, userId) {
    // Check whether the activity exists based on the activityId
    // Get the stage statuses from Redis (Redis stores the stage statuses of the campaign posters in a Hash structure, with key being the posterId (unique for a user within a campaign) and field being the stageId)
    if (status == null) {
        // The status data does not exist. Query whether the database has the record of claiming the prize
        if (exist) {
            // Synchronize to redis and return
            return; }}else (status) {
        return;
    }
    /* If no prize is claimed, claim the prize */
    // Query phase
    // Save to database
    // Write the status to Redis
}    
Copy the code

The complete code

Design Case

Case 1,2, and 4 are add-ons

Case 1: Infrastructure dependency

Using the @springBooTest annotation requires configuring dependencies to start

@Slf4j
@SpringBootTest
public class Case1Test {

    @Test
    // This is the Junit 5 annotation, alias
    @displayName (" Infrastructure dependent tests ")
    void infrastructureRequired(a) {
        log.info("Need to rely on infrastructure."); }}Copy the code

Case 2: No infrastructure dependency

@Slf4j
@Import({ActivityService.class})
@ExtendWith(SpringExtension.class)
public class Case2Test {
    @Resource
    private ActivityService service;
    @MockBean
    private ActivityRepository repository;
    @MockBean
    private RedisTemplate<String, Object> redisTemplate;

    @Test
    @displayName (" no reliance on infrastructure tests ")
    void noInfrastructureRequired(a) {
        log.info("No infrastructure needed to operate."); Assertions.assertNotNull(service); Assertions.assertNotNull(repository); Assertions.assertNotNull(redisTemplate); }}Copy the code

As you can see, there is no need to start the Spring container

Case 3: Award (

As mentioned earlier, the award() method queries the database as well as Redis, so you need to Mock and Stub this part of the operation.

Stub # —– stub {num} —– with Mockito

Because of the presence of branch control statements, only one base path is demonstrated for unit testing, and most stub 1,2,3,5,6,7 is covered

// com.jingwu.example.service.ActivityService#award
public void award(AwardDTO dto) {
    String id = dto.getActivityId(), stageId = dto.getStageId(), userId = dto.getUserId();
    # ----- stub 1 -----
    final ActivityDO activity = repository.selectById(id);
    if (Objects.isNull(activity)) throw new RuntimeException();

    String hashKey = String.format(FISSION_POSTER_AWARD, dto.getPosterId());
    String key = String.valueOf(stageId);
    
    # ----- stub 2 -----
    Object result = redisTemplate.opsForHash().get(hashKey, key);
    if (Objects.isNull(result)) {
        # ----- stub 3 -----
        Boolean exist = repository.exist(id, stageId, userId);
        if (exist) {
            # ----- stub 4 -----
            redisTemplate.opsForHash().put(hashKey, key, true);
            redisTemplate.expire(hashKey, 2L, TimeUnit.HOURS);
            return; }}else if ((Boolean) result) {
        return;
    }
    # ----- stub 5 -----
    ActivityStageDO stage = repository.selectStage(stageId, id);
    if (Objects.isNull(stage)) throw new RuntimeException();
    
    ActivityStageAwardDO entity = new ActivityStageAwardDO()
        .setActivityId(id).setStageId(stageId)
        .setUserId(userId).setStageNum(stage.getStageNum());

    # ----- stub 6 -----
    repository.saveAward(entity);
    # ----- stub 7 -----
    redisTemplate.opsForHash().put(hashKey, key, true);
    redisTemplate.expire(hashKey, 2L, TimeUnit.HOURS);
}
Copy the code

Stub 0

  • chooseaward()Method,Ctrl+Shift+T, select the method you want to unit test, and press Enter to createActivityServiceTest

  • Because ActivityService is unit tested, inject the Bean with the annotation @import ({activityService.class}) (see the Spring Boot unit Test Practices section @import).

  • ActivityService relies on ActivityRepository and RedisTemplate, while in this unit test the focus is on the business logic of the award() method. Regardless of whether the beans in the Spring container actually exist or can be injected, So you can Mock the injected dependent beans with the @MockBean annotation provided by Spring Boot Test (Mock as many beans as there are Bean dependencies, otherwise IllegalStateException: Failed to load ApplicationContext)

@Import({ActivityService.class})
@ExtendWith(SpringExtension.class)
public class ActivityServiceTest {

    @Resource
    private ActivityService service;
    @MockBean
    private ActivityRepository repository;
    @MockBean
    private RedisTemplate<String, Object> redisTemplate;
    
}
Copy the code

Stub 1

The award() method executes the repository. SelectById (id) statement, and the Repository handles the database, so you need to mock/stub to replace the actual JDBC operation.

You can stub using doReturn().when() or when().thenreturn ().

DoReturn and thenReturn have the same effect on Mock objects, only the syntax is different, and only when using Spy objects (see Case 4)

    repository.selectById(id)  
    
=>  ActivityDO activity = mockActivity();
    doReturn(activity).when(repository).selectById(ACTIVITY_ID);
/ / or
=>  when(repository.selectById(ACTIVITY_ID)).thenReturn(activity);    
Copy the code

Stub 2

    redisTemplate.opsForHash().get(hashKey, key)
Copy the code

Because redistemplate.opsForHash ().get(hashKey, key) is a chain operation that requires a step stub, opsForHash returns a package access object, DefaultHashOperations, This class is not accessible in its own package directory, so how do you Mock this object?

Create your own package with the same path, and then create a custom public class that extends DefaultHashOperations.

MyDefaultHashOperations mockOpt = mock(MyDefaultHashOperations.class);
doReturn(mockOpt).when(redisTemplate).opsForHash();
doReturn(null).when(mockOpt).get(any(), any());
Copy the code

Return mockOpt when executed to redistemplate.opsforhash () in the award() method, which then returns null when mockOpt calls the get() method again (to walk into the if branch).

See the Mockito operation for any()

Another way to do this is to encapsulate the helper class to perform the Redis operation (decouple the ActivityService from RedisTeamplate) and mock the helper class once.


@MockBean
privateRedisHelper helper; method(){ ... helper.hget(key, field); . }@Testmethod(){ ... doReturn(object).when(helper).hget(any(), any()); . }Copy the code

When mocks and stubs of business logic are hard to follow, chances are there is something wrong with the structure of the code that needs to be adjusted and refactored on a small scale.

Stub 7

    redisTemplate.opsForHash().put(hashKey, key, true);
    redisTemplate.expire(hashKey, 2L, TimeUnit.HOURS);
        
=>  doNothing().when(spyOpt).put(any(), any(), any());
    doReturn(true).when(redisTemplate).expire(anyString(), anyLong(), eq(TimeUnit.HOURS));
Copy the code

The void method uses doNothing to Stub; See Arbitrary parameter for passing mock methods

Stub 5 and 6 refer to Stub 1. Stub 4 is not in the test path. Stub mode refers to Stub 7

Complete the Case

@Slf4j
@Import({ActivityService.class})
@ExtendWith(SpringExtension.class)
public class ActivityServiceTest {

    @Resource
    private ActivityService service;
    @MockBean
    private ActivityRepository repository;
    @MockBean
    private RedisTemplate<String, Object> redisTemplate;

    private final Fairy fairy = Fairy.create(Locale.CHINA);

    private static final String ACTIVITY_ID = "1";
    private static final String POSTER_ID = "10";
    private static final String STAGE_ID = "100";

    @SuppressWarnings("unchecked")
    @Test
    @displayName (" Test for Collecting prizes during activity Phase ")
    void award(a) {
        AwardDTO dto = new AwardDTO();
        dto.setActivityId(ACTIVITY_ID);
        dto.setStageId(STAGE_ID);
        dto.setPosterId(POSTER_ID);
        ActivityDO activity = mockActivity();

        MyDefaultHashOperations mockOpt = mock(MyDefaultHashOperations.class);
        doReturn(mockOpt).when(redisTemplate).opsForHash();
        doReturn(null).when(mockOpt).get(any(), any());
        doReturn(activity).when(repository).selectById(ACTIVITY_ID);
        doReturn(mockStage()).when(repository).selectStage(any(), any());
        doReturn(false).when(repository).exist(any(), any(), any());
        when(repository.saveAward(any())).thenReturn(true);
        doNothing().when(spyOpt).put(any(), any(), any());
        doReturn(true).when(redisTemplate).expire(anyString(), anyLong(), eq(TimeUnit.HOURS));

        service.award(dto);

        verify(repository, times(1)).saveAward(any());
        verify(redisTemplate, times(2)).opsForHash();
        verify(redisTemplate, times(1)).expire(anyString(), anyLong(), eq(TimeUnit.HOURS));
        verify(spyOpt, times(1)).put(any(), any(), any()); }}Copy the code

Case 4: doReturn vs. thenReturn

When working with a Spy, thenReturn calls the real method and returns the mock data. When working with a Spy, doReturn returns the mock data. The actual method is not called.

public class ActivityRepository {
    public Boolean saveAward(ActivityStageAwardDO entity) {
        final boolean result = RandomUtil.randomBoolean();
        log.info("Save the result: {}", result);
        function();
        return result;
    }
    private void function(a) {
        log.info("Throw an exception.");
        throw newNullPointerException(); }}public class Case4Test {
    @BeforeEach
    void setUp(a) {
        log.info("---- UT Start ----");
    }

    @AfterEach
    void tearDown(a) {
        log.info("---- UT End ----\n");
    }

    @Test
    void doReturnTest(a) {
        Assertions.assertDoesNotThrow(() -> {
            final ActivityRepository spy = spy(ActivityRepository.class);
            doReturn(true).when(spy).saveAward(any());
            spy.saveAward(mockStageAward());
        });
    }

    @Test
    void thenReturnTest(a) {
        Assertions.assertThrows(NullPointerException.class, () -> {
            final ActivityRepository spy = spy(ActivityRepository.class);
            when(spy.saveAward(any())).thenReturn(true); spy.saveAward(mockStageAward()); }); }}Copy the code

Mockito operation

Continuous execution

# stub
// The first execution returns true and the second false. 1 and 2 are equivalent
1. doReturn(true).doReturn(false).when(repository).exist(ACTIVITY_ID);

2. when(repository.exist(ACTIVITY_ID)).thenReturn(true).thenReturn(false);
       

method(id) {
    bool r1 = repository.exist(id); // r1 = true
    // do something()
    bool r2 = repository.exist(id); // r2 = flase
}

Copy the code

Stub and the cords

Parameter matcher

See org. Mockito. ArgumentMatchers

Fixed parameters

  doReturn(activity).when(repository).selectById(ACTIVITY_ID);
Copy the code

The above statement indicates that the mock Activity object is returned when the award() method executes the Repository.selectByid () statement with ACTIVITY_ID. If the passed parameter is not equal to ACTIVITY_ID, stub is not performed.

Any parameters

  doReturn(activity).when(repository).selectById(any());
Copy the code

The above statement indicates that the award() method returns the activity object when it executes the Repository.selectByid () statement argument with any value.

Parameter matchers of specific parameter types can be used, such as

selectById(Long id)  => anyLong()
Copy the code

A variety of parameters

    redisTemplate.expire(hashKey, 2L, TimeUnit.HOURS);
    
=>  doReturn(true).when(redisTemplate).expire(anyString(), anyLong(), eq(TimeUnit.HOURS));
    / / or
    doReturn(true).when(redisTemplate).expire(anyString(), eq(2L), eq(TimeUnit.HOURS));
    / / or
    Long time = 2L;
    doReturn(true).when(redisTemplate).expire(anyString(), eq(time), eq(TimeUnit.HOURS));
   
    doReturn(true).when(redisTemplate).expire(anyString(), eq(2L), eq(TimeUnit.HOURS));   
    doReturn(true).when(redisTemplate).expire(anyString(), anyLong(), any());
    doReturn(true).when(redisTemplate).expire(any(), anyLong(), any()); .Copy the code

When using an argument matcher, all arguments must be matcher style, not allowed to have some arguments with fixed values and some arguments with matchers. Constant/fixed values need to be wrapped with eq().

Parameter matcher has a lot of combinations, more flexible, interested to try their own.

Mockito Mockito Mockito Mockito Mockito Mockito Mockito

conclusion

Unit tests should focus only on the business logic of the current method; any other external dependencies should be mocked.

Jennifer. SpringBootTest should be used for integration testing. It is not recommended for unit testing that is not particularly necessary.

In the end, this article only shows one case to demonstrate how to mock, but the idea is pretty much the same, and we’ll output some related test cases to illustrate it.

other

Unit test coverage

IDEA support Coverage view, test directory or test class right-click Run ‘xxTest’ with Coverage

This enables you to write different test cases for different test paths

Red is uncovered and green is covered

In addition, by setting quality control in CI/CD with Jacoco, any pipeline with unit test coverage below that level will fail and will not be allowed to test, release, or go live (as such, only 10% will probably prevent most projects from going live).

Fairy (Mock data)

// java.util.Locale Specifies the Locale. By default, ENGLISH private final Fairy Fairy = fairy.create (locale.china); @Test void fairy() { Person person = fairy.person(); Company company = fairy.company(); CreditCard creditCard = fairy.creditCard(); TextProducer textProducer = fairy.textProducer(); BaseProducer baseProducer = fairy.baseProducer(); DateProducer dateProducer = fairy.dateProducer(); NetworkProducer networkProducer = fairy.networkProducer(); }Copy the code

jFairy by Codearte

Complete reliance on

<dependencies>
     <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.codearte.jfairy</groupId>
        <artifactId>jfairy</artifactId>
        <version>0.5.9</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.3.12. RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
Copy the code

The business logic

/ * * *@authorKim called *@since2021/7/22 - did I * /
@Service
public class ActivityService {

    private static final String FISSION_POSTER_AWARD = "activity:poster:award:%s";
    private final ActivityRepository repository;
    private final RedisTemplate<String, Object> redisTemplate;

    public ActivityService(ActivityRepository repository, RedisTemplate<String, Object> redisTemplate) {
        this.repository = repository;
        this.redisTemplate = redisTemplate;
    }

    public void award(AwardDTO dto) {
        String id = dto.getActivityId();
        String stageId = dto.getStageId();
        String userId = dto.getUserId();

        final ActivityDO activity = repository.selectById(id);
        if (Objects.isNull(activity)) {
            throw new RuntimeException();
        }

        String hashKey = String.format(FISSION_POSTER_AWARD, dto.getPosterId());
        String key = String.valueOf(stageId);

        Object result = redisTemplate.opsForHash().get(hashKey, key);
        if (Objects.isNull(result)) {
            Boolean exist = repository.exist(id, stageId, userId);
            if (exist) {
                redisTemplate.opsForHash().put(hashKey, key, true);
                redisTemplate.expire(hashKey, 2L, TimeUnit.HOURS);
                return; }}else if ((Boolean) result) {
            return;
        }

        ActivityStageDO stage = repository.selectStage(stageId, id);
        if (Objects.isNull(stage)) {
            throw new RuntimeException();
        }
        ActivityStageAwardDO entity = new ActivityStageAwardDO()
                .setActivityId(id).setStageId(stageId)
                .setUserId(userId).setStageNum(stage.getStageNum());

        repository.saveAward(entity);

        redisTemplate.opsForHash().put(hashKey, key, true);
        redisTemplate.expire(hashKey, 2L, TimeUnit.HOURS); }}Copy the code

The project address

Spring Boot UT Junit5

Attached part of the reference article, more content please search -.-

  • Junit 5 Official document Chinese version
  • This section describes common usage of JUnit 5
  • Introduction to Mock simulation testing and introduction to using Mockito

reference


  1. The mock test↩
  2. Pile (computer) – Wikipedia, the free encyclopedia↩