SpringBoot2.3整合Mockito实现单元测试

导读:本篇文章讲解 SpringBoot2.3整合Mockito实现单元测试,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

1. 概述

Mockito是一个用于Java单元测试的优秀强大的框架,当需要调用第三方接口而开发测试环境又无法直接调用此接口时,就可以使用Mockito模拟接口调用编写完美的单元测试,这样也使得与第三方应用进行了强解耦,更多详情请参阅Mockito官网

2. 引入Mockito依赖

由于SpringBoot自身整合了Mockito,所以在整合Mockito编写单元测试的时候,只需要引入test依赖即可

<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>

另外,Mockito在junit4和junit5中使用的注解有些区别,如果是使用junit4进行单元测试,还需引入junit4的依赖

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

3. 接口代码编写

本文使用SpringBoot2.3.12和MyBatis-Plus3.4.2编写Dao层和Service层接口,这里不做详细讲解,有不清楚如何使用的小伙伴请参阅SpringBoot2.3.4整合MyBatis-Plus3.4.0和Swagger3.0,此外,还会用到mapstruct,具体使用方法可参阅SpringBoot2.3整合MapStruct实现Java bean映射

4. 编写测试类

4.1. 测试类中引入mock

在测试类中引入mock有两种方法,一种是在代码中导入静态方法mock,另一种是使用注解@Mock。官网推荐使用注解方式引入mock,其优点如下:

  1. 最大限度地减少重复的模拟创建代码
  2. 使测试类更具可读性
  3. 使验证错误更易于阅读,因为字段名称用于标识模拟

注意:使用@Mock注解时,需要添加运行器使注解生效
junit4中有三种方法,具体如下:

  1. 初始化mock:MockitoAnnotations.initMocks(this)
  2. 测试类上使用注解MockitoJUnitRunner
  3. 使用MockitoRule

示例代码如下:

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

其中MockitoJUnitRunner有三种方式

  1. Silent:实现忽略存根参数不匹配 (MockitoJUnitRunner.StrictStubs) 并且不检测未使用的存根
  2. Strict:检测未使用的存根并将它们报告为失败
  3. StrictStubs:改进调试测试,帮助保持测试干净

MockitoRule有两种方式

  1. silent():规则不会在测试执行期间报告存根警告
  2. strictness(Strictness strictness):严格级别,尤其是“严格存根”(Strictness.STRICT_STUBS)有助于调试和保持测试清洁,另外还有严格级别LENIENT和WARN

junit5中也有三种方法,具体如下:

  1. 初始化mock:MockitoAnnotations.initMocks(this)
  2. 测试类上使用注解@ExtendWith(MockitoExtension.class)
  3. 测试类上使用注解@MockitoSettings,为测试类配置使用的严格等级

示例代码如下:

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

4.2. 常用注解

mock创建的注解有
@Mock:用于创建和注入模拟实例
@Spy:监视现有实例
@InjectMocks:将模拟字段自动注入到测试对象中
@Captor:用于获取参数
@Mock与@Spy的区别

  • 使用@Mock创建一个mock时,是从类型的类中创建的,而不是从实际实例中创建,mock只是创建了一个类的基本外壳实例,用于跟踪与其的交互。
  • 使用@Spy时,将包装一个现有的实例,除了行为方式与普通实例相同外,还将检测以跟踪与它的所有交互。
  • 通常情况下@Mock用于访问第三方服务接口,@Spy用于访问本服务接口(读取配置类或者mapstruct接口)

实例代码如下:

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(存根/打桩)

使用when().thenReturn()存根有返回值的接口,使用when().thenThrow()存根有异常的接口,使用when().thenAnswer()存根有回调函数的接口,使用given().willReturn()存根BDD格式有返回值接口,第一个()中是mock对象的方法调用,第二个()中是返回的对象。
两种的功能都是一样的,given()是行为驱动开发BDD(Behavior Driven Development)的风格格式。
示例代码如下:

@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);
    // 或者
    // BDDMockito.given(userInfoMapper.selectById(any())).willReturn(userInfo);
    final SysUserInfo info = userInfoService.getById(1);
    Assertions.assertNotNull(info);
}

当以下情况时,使用带do的方法

  • 存根void方法
  • 在spy对象上存根方法
  • 多次存根相同的方法,以在测试过程中更改模拟的行为

常用的带do的方法有doThrow()、doAnswer()、doNothing()、doReturn()和doCallRealMethod()
doReturn():用于当监视真实对象并在spy上调用真实方法时会带来副作用,或覆盖先前的异常存根
doThrow():用于给void方法存根需要有异常抛出
doAnswer():用于给void方法存根需要有回调值
doNothing():用于给void方法存根不需要做任何事,使用情况为对void方法连续调用进行存根或者监视真实对象且void方法不执行任何操作
doCallRealMethod():用于调用真正执行的方法

4.4. 验证

verify
用于验证某些行为至少发生过一次,例如:

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

或者

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

验证某些行为发生了至少一次/确切的次数/从未使用
atLeastOnce():至少发生一次

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

atLeast(num):至少发生num次

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

atMostOnce():最多发生一次

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

atMost(num):最多发生num次

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

never():从未发生过

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

only():校验的方法是否是唯一调用

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

timeout():给定的时间(毫秒)内一直触发验证,可以用于测试异步代码

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

after():给定的时间(毫秒)后触发验证,可以测试异步代码

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

timeout()与after()区别

  • timeout()验证通过后立即成功退出
  • after()等待给定的时间后才开始验证

ArgumentCaptor
用于获取请求参数以进行进一步断言,通常结合verify()一起使用,其适用条件如下:

  • 自定义参数匹配器不能被重用
  • 只需要对参数值进行断言即可验证

测试类中引入ArgumentCaptor可以使用注解@Captor,也可以使用

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

其主要方法有capture()、getValue()、getAllValues()
capture():用于捕获参数值,必须在验证内部使用此方法

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

getValue():返回捕获的参数值,如果验证方法被多次调用,只返回最新捕获的值

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

getAllValues():返回捕获的所有参数值,用于捕获可变参数或多次调用验证方法,当多次调用varargs方法时,返回来自所有调用的所有值的合并列表
InOrder
用于按顺序验证mock对象,可以只验证需要的mock对象

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

calls():允许按顺序进行非贪婪验证

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

与times(1)不同的是,如果该方法调用了2次也不会报错
与atLeast(1)不同的是,不会将第二次标记为已验证

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/76782.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!