# 单元测试小技巧

# 前言

# 工具

  1. junit5
  2. 实用Mockio进行mock
 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.3.7.RELEASE</version>
  </dependency>

目的:单测覆盖率100%!!!!

# 插件推荐

# Squaretest

写单测算是个比较无聊且冗长的事情,但这是规范!(手动狗头!),要是有个插件能帮我们处理一些事就是极好的。
这里推荐一下Squaretest(免费30天),可以帮我们把模板代码生成,对于不会写单测或者说对写单测不太熟悉的同学,就是一个很不错的切入点。

# 实用小技巧

# 1. 查看局部代码的单测覆盖率

  1. 右键选中要查看覆盖率的子包
  2. 选择More Run/Debug
  3. 选中Run Test in "你选中的子包" With Coverage

单元测试1

# 代码测试及参数mock

# 1.有返回参数的方法怎么mock

一般来说,单元测试的目的是校验自己项目本身的代码逻辑,并不会要求跨服务校验。所以这种情况,我们只需要对第三方服务进行mock,用我们期望的返回值去校验本服务的逻辑就好。

# 待测试代码

package com.cn.liuxiaolxx.test;

@Service
public class TestServiceImpl implements TestService {
    
    @DubboReference(version = "1.0.0")
    private TestRemoteService testRemoteService;
    
    @Override
    public BigDecimal queryList(DescribeReqeust request) {

        return testRemoteService.query(request);
    }
}

# 单测代码书写

package com.cn.liuxiaolxx.test;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class TestServiceImplTest {

    /**
     * 使用注解@Mock对TestRemoteService服务进行mock
     */
    @Mock
    private TestRemoteService testRemoteService;

    /**
     * 使用注解@InjectMocks将该service之前声明的被@Mock标记的service注入到本服务中
     */
    @InjectMocks
    private TestServiceImpl testService;

    @Test
    public void testQueryList_success_return_result() {
        DescribeReqeust request = new DescribeReqeust();
        
        // 当调用testRemoteService.query方法时,无视参数,直接返回10
        when(testRemoteService.query(eq(request))).thenReturn(BigDecimal.TEN);

        // 调用
        BigDecimal actualResult = testService.queryList(request);
        
        // 结果验证
        assertThat(actualResult).isEqualTo(BigDecimal.TEN);
        // 调用次数验证,默认校验调用1次
        verify(testRemoteService).query(eq(request));
    }
}

# 2.无返回参数的方法怎么mock

# 待测试代码

package com.cn.liuxiaolxx.test;

@Service
public class TestServiceImpl implements TestService {

    @Autowired
    private TestMapper testMapper;
    
    @Autowired
    private TestCalculateService testCalculateService;

    @Override
    public void calculate() {
        List<BigDecimal> emptyList = Lists.newArrayList();
        
        testCalculateService.calculate(emptyList);
        
        if (CollectionUtils.isNotEmpty(emptyList)) {
            testMapper.insertOrUpdate(emptyList);
        }
    }
}

# 单测代码书写

package com.cn.liuxiaolxx.test;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;


@ExtendWith(MockitoExtension.class)
public class TestServiceImplTest {

    @Mock
    private TestMapper testMapper;

    @Mock
    private TestCalculateService testCalculateService;

    /**
     * 使用注解@InjectMocks将该service之前声明的被@Mock标记的service注入到本服务中
     */
    @InjectMocks
    private TestServiceImpl testService;

    @Test
    public void testCalculate() {
        
        Mockito.doAnswer((Answer<Object>)invocation -> {
            // 获取第0个参数,若有多个参数的情况,按下标获取即可
            List<BigDecimal> priceList = invocation.getArgument(0);

            // 在参数中加入1和10
            priceList.add(BigDecimal.ONE);
            priceList.add(BigDecimal.TEN);

            return invocation;
            // 当调用testCalculateService的calculate的时候,无视参数
        }).when(testCalculateService).calculate(any());

        // 当调用testMapper.insertOrUpdate方法时,直接返回成功(影响条数1)
        when(testMapper.insertOrUpdate(any())).thenReturn(1);

        // 方法调用
        testService.calculate();
        // 调用次数验证,默认校验调用1次
        verify(testCalculateService).calculate(any());
        // 调用次数验证,由于list中插入了2条数据,需要验证调用2次
        verify(testMapper, times(2)).insertOrUpdate(any());
    }
}

# 3.分页处理的任务怎么mock

# 待测试代码

package com.cn.liuxiaolxx.test;

import javax.management.Query;

@Service
public class TestServiceImpl implements TestService {

    @Autowired
    private TestMapper testMapper;

    @Autowired
    private Test2Service test2Service;

    @Override
    public void calculate(String parameter) {
        Query query = new Query();
        // query set other params
        query.setParameter(parameter);
        
        int page = 1;
        int size = 1000;

        while (true) {

            query.setCurrent(page);
            query.setSize(size);

            List<Result> resultList = testMapper.testQuery(query);
            if (CollectionUtils.isEmpty(resultList)) {
                break;
            }

            test2Service.doSomething(resultList);
            page++;
        }
    }
}

# 单测代码书写

这时候我们观察自己的待检测代码,如果还按照之前的方式进行mock,那循环就结束不了了。所以期望在指定参数的情况下,才返回结果。其他时候走break逻辑

package com.cn.liuxiaolxx.test;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class TestServiceImplTest {

    @Mock
    private TestMapper testMapper;

    @Mock
    private Test2Service test2Service;

    /**
     * 使用注解@InjectMocks将该service之前声明的被@Mock标记的service注入到本服务中
     */
    @InjectMocks
    private TestServiceImpl testService;

    @Test
    public void testCalculate() {
        // 入参
        String parameter = "parameter";
        
        Result result = new Result();
        List<Result> resultList = Arrays.asList(result);
        // 当参数query的current是1的情况下,才返回指定结果
        when(testMapper.testQuery(Mockito.argThat(
            query -> query.getCurrent() == 1))).thenReturn(resultList);
        
        // 方法调用
        testService.calculate(parameter);
        
        // 调用次数验证,默认校验调用1次
        verify(test2Service).doSomething(eq(resultList));
        // 调用次数验证, 第一次查询成功,进入逻辑处理,第2次查询为空,跳出循环,需要验证调用2次
        verify(testMapper, times(2)).testQuery(any());
    }
}

# 4.校验指定异常

# 待测试代码

package com.cn.liuxiaolxx.test;

@Service
public class TestServiceImpl implements TestService {

    @Override
    public void calculate(String parameter) {
       if (Strings.isNullOrEmpty(parameter)) {
           throw new RuntimeException("参数异常, parameter不能为空");
       }
       
       // doSomething
    }
}

# 单测代码书写

这时候我们观察自己的待检测代码,如果还按照之前的方式进行mock,那循环就结束不了了。所以期望在指定参数的情况下,才返回结果。其他时候走break逻辑

package com.cn.liuxiaolxx.test;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class TestServiceImplTest {

    @Mock
    private TestServiceImpl testService;

    @Test
    public void testCalculate() {
        // 入参
        String parameter = null;
        
        // 方法调用
        try {
            testService.calculate(parameter);
            Assertions.fail("参数异常了,检测后必须抛异常");
        } catch (RuntimeException e) {
            assertThat(e.getMessage()).isEqualTo("参数异常, parameter不能为空");
        }
    }
}

# 5. 参数化校验


import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * The type IntelliJ IDEA.
 * <p>
 *
 * @author liuxiaolu
 * @since 2022/7/8
 */
@ExtendWith(MockitoExtension.class)
class Test {
    @ParameterizedTest
    @CsvSource(value = {",,0", "-1,-1,-2", "1,,1", ",1,1"})
    void test(Integer a, Integer b, Integer result) {
        assertThat(result).isEqualTo(add(a, b)).withFailMessage("计算失败,期望结果是" + result);
    }

    Integer add(Integer a, Integer b) {
        if (a == null && b == null) {
            return 0;
        }

        if (a == null) {
            return b;
        }

        if (b == null) {
            return a;
        }

        return a + b;
    }
}

# 6. mock重置

在我们写单测的过程中,一般会在一个类中写很多个case,对于每个case我们可能需要mock不同的方法,不同的返回值。在每次mock之后,需要将mock重置,否则可能出现存根异常。

@ExtendWith(MockitoExtension.class)
class AllowanceCalculateServiceTest {

    @Mock
    private TestMock1 testMock1;

    @Mock
    private TestMock2 testMock2;
    
    @InjectMocks
    private TestService testService;

    /**
     * 在每个case调用完成之后,重置mock
     */
    @AfterEach
    void reset() {
        Mockito.reset(testMock1, testMock2);
    }
    
    // other method
}

# 999.拓展点

# 1. countDownLatch.await()怎么抛InterruptedException

# 待测试代码
package com.cn.liuxiaolxx.test;

@Slf4j
@Service
public class TestServiceImpl implements TestService {

    @Autowired
    private TestCalculateService testCalculateService;
    
    @Override
    public void calculate() {
        CountDownLatch countDownLatch = new CountDownLatch(1);

        try {
            testCalculateService.doSomeThing();
        } catch (Exception e) {
            log.error("逻辑处理异常", Throwables.getStackTraceAsString(e));
        } finally {
            countDownLatch.countDown();
        }
        
        
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            log.error("主线程中断,", e);
        }
    }
}
# 测试示例

通过源码分析我们可以知道,countDownLatch.await()在主线程被中断的时候会抛出异常InterruptedException, 那我们怎么测试抛出InterruptedException异常呢?

package com.cn.liuxiaolxx.test;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class TestServiceImplTest {

    @Mock
    private TestCalculateService testCalculateService;

    @Test
    public void testCalculate_excepted_check_interrupted_exception() {

        // 获取主线程
        Thread mainThread = Thread.currentThread();

        // 当调用testCalculateService的calculate的时候,调用代码块中的逻辑
        Mockito.doAnswer((Answer<Object>)invocation -> {

            // 在子线程中中断主线程
            mainThread.interrupt();
            return invocation;
        }).when(testCalculateService).doSomeThing();

        // 方法调用
        testService.calculate();

        // 调用次数验证,默认校验调用1次
        verify(testCalculateService).doSomeThing(eq(resultList));
    }
}
Last Updated: 8/11/2022, 11:33:46 PM