Mockito单测之道

Mockito单测之道

去年写过一篇《TestNG单元测试实战》文章,严格来讲算集成测试。

没看的小伙伴可直接看本篇即可,本质是单元测试框架不同,写法不一样。

单测定义

单元测试定义:

对软件中最小可测单元进行验证,可理解为对一个类中公有、私有方法的测试验证。

单元测试原则:

1. 快速的。
不依赖外部环境比如数据库mysql,不依赖springboot应用启动,本质是执行一个函数,一个方法,所以理应是能快速运行的。

2. 自动的。
单元测试应该是全自动执行的,非交互式的。

3. 独立的。
单元测试用例之间是独立运行的,用例互相之间无依赖,对外部资源也无依赖。

4. 可重复的。
单元测试是可重复执行的,在被测代码不变的情况下,每次执行结果是一致的。

单元测试用例主要有以下特点:

1. 不依赖外部环境和数据;
2. 不需要启动应用和初始化对象;
3. 需要使用@Mock来初始化依赖对象,用@InjectMocks来初始化测试对象;
4. 需要自己模拟依赖方法,指定什么参数返回什么值或异常;
5. 返回值确定,可用Assert相关方法进行断言;
6. 可以验证依赖方法的调用次数和参数值,还可以验证依赖对象的方法是否调用完毕。

什么是有效的单元测试

  1. 验证依赖方法,验证调用次数;

  2. 使用明确语义的断言;

  3. 验证异常抛出;

  4. 验证数据对象属性;

  5. 用mock对象代替真实对象的注入;
    ......

单元测试和集成测试区别:

单元测试是对软件设计中最小单元的程序模块进行测试,集成测试是对这些程序模块组装成的系统模块进行测试。(单元测试没有外部依赖,集成测试也可能没有外部依赖)

集成测试用例主要有以下特点:

1. 依赖外部环境和数据;
2. 需要启动应用并初始化测试对象;
3. 使用自动注入机制注入测试对象;
4. 返回值具有不确定性,无法验证。

综上所述,为了更好的验证软件质量,更容易的去测试业务逻辑,推荐编写单元测试来完成代码的用例编写。

实战

环境依赖引入

springboot版本:2.2.2.RELEASE

引入Maven:



    org.springframework.boot
    spring-boot-starter-test
    test

spring-boot-starter-test 依赖中包含了junit 和 mockito框架。

本文用Junit5 + Mockito 来编写单测。

Junit5 = Junit Platform + Junit Jupiter + Junit Vintage

IDEA工具推荐下载 TestMe 插件,自动生成用例类,生成通用模板后再进行修改。

Mockito单测之道_第1张图片

Mockito单测之道_第2张图片

项目案例编写

从一个简单的例子入手

被测 service 对象:

@Service
public class UserServiceImpl {

    @Autowired
    private IOrderService orderService;

    private static List ids;

    @Override
    public int activeNumber() {
        LambdaQueryWrapper wrapper = Wrappers.lambdaQuery()
                .in(Order::getId, ids);
        int count = orderService.count(wrapper);
        return count;
    }
}

单元测试用例类:

public class UserServiceImplTest {

    /**
     * 定义测试对象
     */
    @InjectMocks
    UserServiceImpl userServiceImpl;

    /**
     * 模拟依赖对象
     */
    @Mock
    IOrderService orderService;

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

    @Test
    void testActiveNumber() {
        Mockito.doReturn(1).when(orderService).count(Mockito.any());
        int result = userServiceImpl.activeNumber();
        Assertions.assertEquals(1, result, "数据结果不一致");
        Mockito.verify(orderService).count(Mockito.any());
    }
}

使Mockito的注解生效:

Junit4中:

  1. 类上添加 @RunWith(MockitoJUnitRunner.class)。

  2. @Before注解方法加 MockitoAnnotations.openMocks(this)。

Junit5中:

  1. 类上添加 @ExtendWith(MockitoExtension.class)。

  2. @BeforeEach注解方法加 MockitoAnnotations.initMocks(this)。(版本不同,写法不一样)

一个有依赖的单元测试实施步骤:

一. 定义被测对象。

用@Mock、@Spy、@InjectMocks 相关注解定义测试对象。

  • @Mock:该注解创建一个Mock对象,调用其方法时不会走真实方法

  • @Spy:该注解创建了一个没有Mock的对象,调用其方法是会走真实方法

  • @InjectMocks:创建一个实例,调用代码会走真实方法。会自动注入@Spy和@Mock标注的对象。

/**
 * 定义测试对象
 */
@InjectMocks
UserServiceImpl userServiceImpl;

/**
 * 模拟依赖对象
 */
@Mock
IOrderService orderService;

如果有静态类成员字段需要注入: 可用ReflectonTestUtils工具注入属性。

ReflectionTestUtils.setField(userServiceImpl, "ids", ids);

二. 模拟依赖对象

模拟依赖对象方法返回,包含参数的返回、异常的返回等。

//模拟orderService.count() 方法调用,调用时返回值为1。
Mockito.doReturn(1).when(orderService).count(Mockito.any());

Mockito.doReturn().when() 和 Mockito.when().thenReturn() 都用于模拟对象方法,在Mock实例下都可使用。

但在 Spy实例下使用时, when().thenReturn() 模式会执行原方法,而 doReturn().when()模式不会执行原方法。

Mock实例下

Mockito.doReturn().when() Mockito.when().thenReturn() 都会走模拟方法。 

Spy实例下

Mockito.doReturn().when() 走模拟调用
Mockito.when().thenReturn() 走真实调用

推荐使用doReturn/when。如果不关心具体的参数内容,可用Mockito.any() 代替。

三. 调用被测对象

//模拟依赖方法
int result = userServiceImpl.activeNumber();
//调用被测对象
Assertions.assertEquals(1, result, "数据结果不一致");
Mockito.verify(orderService).count(Mockito.any());

代码中的例子比较简单,假如要模拟的参数是一个对象,则可用JSONObject来验证,提前创建json字符串在文件中,最终转成字符串比较即可。

//例子  

String text = ResourceHelper.getResourceAsString(getClass(), "text.json");
List list = JSON.parseArray(text, T.class);
Mockito.doReturn(list).when(service).list(Mockito.any());
  
//调用依赖方法
List> result = serviceA.method();

String text_expected = ResourceHelper.getResourceAsString(getClass(), "text_expected.json");

//映射排序字段,保证key、value字段有序性
Assertions.assertEquals(text_expected, JSON.toJSONString(result, SerializerFeature.MapSortField));
Mockito.verify(service).list(Mockito.any());
//getResourceAsString() 来自参考资料。
  
public static  String getResourceAsString(Class clazz, String name) {
    try (InputStream is = clazz.getClassLoader().getResourceAsStream(name)) {
        return IOUtils.toString(is, StandardCharsets.UTF_8);
    } catch (IOException e) {
        throw new IllegalArgumentException(String.format("以字符串方式获取资源(%s)异常", name), e);
    }
}

四. 验证方法调用

确认被测方法是否按预期方法进行了调用。

//验证依赖方法,默认验证调用一次
Mockito.verify(orderService).count(Mockito.any());

验证多次调用可通过 Mockito.times()调整,Mockito还支持 最少一次调用最多一次调用等方法验证。

验证调用3次

Mockito.verify(service, Mockito.times(3)).count(Mockito.any());

也可通过 verifyNoMoreInteractions() 验证依赖对象,以确保所有调用都得到验证。

单测技巧

1. 使用JSON序列化简化预期值的比较。

在test/resources 目录下,定义预期值的 json文件。减少验证对象代码的编写。

//和 getResourceAsString() 方法类似,读取json文件数据。
  
  
try {
    InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("testActiveNumberWithResult.txt");
    String text = IOUtils.toString(is, "UTF-8");
    //JSON转成对应的list、map即可
    List list = JSON.parseObject(text, List.class);
} catch (IOException e) {
    //捕获异常
    throw new RuntimeException(e);
}

2、通过Assert.assertThrows 来验证方法异常抛出。

支持验证自定义属性、支持验证依赖方法及其参数。

Assertions.assertThrows(RuntimeException.class, () -> Integer.parseInt("1"), "未返回期望异常");

3. ArgumentCaptor 类捕获参数值。

Mockito 提供 ArgumentCaptor 类来捕获参数值,通过调用 forClass(Class clazz) 方法来构建一个 ArgumentCaptor 对象,然后在验证方法调用时来捕获参数,最后获取到捕获的参数值并验证。

如果一个方法有多个参数都要捕获并验证,那就需要创建多个 ArgumentCaptor 对象。

ArgumentCaptor 的主要接口方法:

  • capture():用于捕获方法参数;

  • getValue():用于获取捕获的参数值,如果捕获了多个参数值,该方法只返回最后一个参数值;

  • getAllValues():用户获取捕获的所有参数值。

//代码实例

ArgumentCaptor captor = ArgumentCaptor.forClass(Wrapper.class);
Mockito.verify(service).list(captor.capture());
Wrapper wrapper = captor.getValue();

参考资料

  1. https://developer.aliyun.com/ebook/7895

你可能感兴趣的:(单元测试,junit,java)