单元测试学习归纳

1 篇文章 0 订阅
1 篇文章 0 订阅

单元测试

为什么要单元测试

1、验证代码的正确性:单元测试可以验证代码在预期输入下是否产生了正确的输出。通过编写针对函数、类或模块的小型测试用例,可以确保代码按照预期工作,减少出现潜在错误的可能性。

2、提供反馈和文档:单元测试可以作为代码的规范和文档,它们描述了每个功能的使用方式和预期行为。当其他开发人员查看或修改代码时,他们可以通过阅读单元测试来理解代码的目的和预期结果。

3、改进设计和可维护性:编写单元测试通常需要将代码分解成更小的组件,这有助于改善代码的设计和可维护性。通过将代码拆分为易于测试的单元,可以更容易地识别和修复潜在的问题,并支持重构和更好的模块化。

4、提高代码质量:通过频繁运行单元测试,可以及早发现代码中的问题,并迅速定位和修复它们。这可以减少在后期开发阶段发现和解决问题所需的时间和资源,提高代码的质量和稳定性。

5、支持重构和持续集成:在进行重构操作时,单元测试可以提供信心,确保代码在更改后仍然按预期工作。此外,单元测试还是持续集成流程的重要组成部分,能够自动运行以验证新代码的正确性,并帮助捕获引入的任何错误。

什么是Mock测试

Mock通常是指,在测试一个对象A时,我们构造一些假的对象来模拟与A之间的交互,而这些Mock对象的行为是我们事先设定且符合预期。通过这些Mock对象来测试A在正常逻辑,异常逻辑或压力情况下工作是否正常。
Mock 测试就是在测试过程中,对于某些不容易构造(如 HttpServletRequest 必须在Servlet 容器中才能构造出来)或者不容易获取比较复杂的对象(如 JDBC 中的ResultSet 对象),用一个虚拟的对象(Mock 对象)来创建以便测试的测试方法。Mock 最大的功能是帮你把单元测试的耦合分解开,如果你的代码对另一个类或者接口有依赖,它能够帮你模拟这些依赖,并帮你验证所调用的依赖的行为。
即mock测试是不需要启动SpringBoot的依赖环境的,注解@SpringBootTest是不应该使用的

且单元测试必须不能依赖第三方接口和组件例如数据库,消息中间件等

官方资料

https://gitcode.net/mirrors/imsingle/mockito-doc-zh?utm_source=csdn_github_accelerator#1

Maven包引入

<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-core</artifactId>
   <version>3.7.7</version>
</dependency>
<dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.13.2</version>
</dependency>
        
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>

单元测试命名规范

以对应方法所属类关联命名:

1、单测的命名空间基与主代码的命名空间一致
2、如果该类逻辑不多,方法也不多,则可以一个测试类对应一个主类(一般这种即可)
3、如果类较复杂或者测试的方法较复杂,建议一个测试类对应一个方法

方法名为6段式(可参考,不必严格执行)

第一段固定为test
第二段为需要测试的方法名,此例为aaa
第三段固定“With”,后面跟着方法输入参数,如果方法没有输入值,则省略with以及第四段
第四段为方法参数描述,多个方法参数用"and"连接,描述上随意,只要能看懂就行,比如此例为bbb,如果过于复杂可以加上注释
第五段固定“Expect”,后面跟着方法的输出结果
第六段为方法的输出结果,此例为ccc,如果预期的结果为异常,也可以加上异常的具体种类

Mockito参数匹配器

Mockito 提供了一系列的参数匹配器,可以根据参数类型和值来匹配参数。
例如 any()、anyXXX()等

单元测试案例

Java的单元测试是以方法为维度的,可能涉及公有方法,静态方法,私有方法

@Slf4j
@Service
public class IdnDebtDelayRecordSpi implements IDebtDelayRecordSpi {

    @Autowired
    private ListingDebtDelayServerFeignClient listingDebtDelayServerFeignClient;

    @Override
    public List<DebtDelayRecordDto> queryDebtDelayRecordByUserId(Long userId) {
        return listingDebtDelayServerFeignClient.queryDebtDelayRecordByUserId(userId).getData();
    }
}

@RunWith(MockitoJUnitRunner.class)
public class IdnDebtDelayRecordSpiTest {

    @Mock
    private ListingDebtDelayServerFeignClient listingDebtDelayServerFeignClient;

    @InjectMocks
    private IdnDebtDelayRecordSpi idnDebtDelayRecordSpi;

    @Test
    public void testQueryDebtDelayRecordByUserId() {
        String json = "{ \"id\": 1165, \"userId\": 216812123, \"debtId\": 209727269, \"bizType\": 2, \"operatorId\": 0, \"flowNo\": \"229bbb7c26bd4dbcaa77dd8c662a01fa\", \"status\": 3, \"launchTime\": \"2023-10-16T11:14:19.000+0000\", \"oldDueDate\": \"2023-10-30T15:59:59.000+0000\", \"newDueDate\": \"2023-11-06T15:59:59.000+0000\" }";
        DebtDelayRecordDto debtInfo = JSON.parseObject(json, DebtDelayRecordDto.class);
        BasicResponse<List<DebtDelayRecordDto>> basicResponse = new BasicResponse<>();
        basicResponse.setCode(0);
        basicResponse.setMessage("SUCCESS");
        List<DebtDelayRecordDto> data = Lists.newArrayList(debtInfo);
        basicResponse.setData(data);
        basicResponse.setSuccess(true);
        Mockito.when(listingDebtDelayServerFeignClient.queryDebtDelayRecordByUserId(216812123L)).thenReturn(basicResponse);
        List<DebtDelayRecordDto> debtDelayRecordDtoList = idnDebtDelayRecordSpi.queryDebtDelayRecordByUserId(216812123L);
        Assert.assertNotNull(debtDelayRecordDtoList);
    }

}

上面的代码是一个的案例

@Mock 和 @InjectMock 的区别

假如我需要测试的目标方法是 IdnDebtDelayRecordSpi 里面的queryDebtDelayRecordByUserId(Long userId)方法,那么我在写测试类的时候,首先需要用 @InjectMocks 注入要被我测试的类,然后因为 queryDebtDelayRecordByUserId 方法中用到了 ListingDebtDelayServerFeignClient ,也就是说依赖了ListingDebtDelayServerFeignClient,所以在测试类中,我又需要使用 @Mock来模拟注入这个 ListingDebtDelayServerFeignClient。

可以简单理解为@InjectMocks就是用在要测试的类上,而@Mock是为了填补@InjectMocks的那个类,这里为IdnDebtDelayRecordSpi里面所注入的成员变量,这里为listingDebtDelayServerFeignClient

私有方法&静态方法注入测试

@Service
@Slf4j
public class SysSmsTemplateConfigureBiz {
		@Autowired
	  private SysSmsTemplateConfigureService smsService;
		@Autowired
    private AuthUserBiz authUserBiz;

    /**
     * 更新短信模板
     */
    public GenericResponse<AddOrUpdateSmsResp> updateSms(SmsUpdateReq req) {
        //1.校验模板名称
        List<SmsResp> smsResps = smsService.queryList(new SmsQueryReq());
        //排除掉自己
        smsResps = smsResps.stream().filter(item -> !item.getId().equals(req.getId())).collect(Collectors.toList());
        if (smsResps.stream().anyMatch(item -> item.getName().equals(req.getName()))) {
            return GenericResponse.ofFail("短信名称已存在");
        }
        Boolean resultFlag = smsService.updateSms(req);
        String existNotAsciiCharacter = AsciiCodeUtils.extractNonASCIICharacters(req.getContent());
        return GenericResponse.ofSuccess(AddOrUpdateSmsResp.builder().existNotAsciiCharacter(existNotAsciiCharacter).resultFlag(resultFlag).build());
    }

    /**
     * 根据userId查询userName
     */
    private String getUserName(Long userId) {

        UserBo user = authUserBiz.getUserById(userId, Boolean.FALSE);
        if (ObjectUtils.isEmpty(user)) {
            return null;
        }
        return user.getName();
    }

}

public class AsciiCodeUtils {

    private static final Pattern PATTERN = Pattern.compile("[^\\x00-\\x7F]");


    private AsciiCodeUtils() {
    }

    /**
     * 提取字符串中的非ASCII字符,返回提取后的字符串,多个非ASCII字符之间用英文分隔
     *
     * @param content
     * @return
     */
    public static String extractNonASCIICharacters(String content) {
        Matcher matcher = PATTERN.matcher(content);
        StringBuilder sb = new StringBuilder();
        while (matcher.find()) {
            int startIndex = matcher.start();
            int endIndex = matcher.end();
            String nonAsciiChar = content.substring(startIndex, endIndex);
            if (StringUtils.isNotBlank(nonAsciiChar)) {
                if (sb.length() > 0) {
                    sb.append(",");
                }
                sb.append(nonAsciiChar);
            }
        }
        return StringUtils.isNotBlank(sb.toString()) ? sb.toString() : null;
    }
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({AsciiCodeUtils.class, SysSmsTemplateConfigureBiz.class})
public class SysSmsTemplateConfigureBizTest {

    @InjectMocks
    private SysSmsTemplateConfigureBiz sysSmsTemplateConfigureBiz;

    @Mock
    private SysSmsTemplateConfigureService smsService;

    @Mock
    private AuthUserBiz authUserBiz;


    @Test
    public void testUpdateSms() {
        Mockito.when(smsService.queryList(new SmsQueryReq())).thenReturn(Mockito.anyList());

        String json = "{\"sendScene\":\"0\",\"name\":\"非ASCII码字符测试02\",\"techniqueId\":\"ASCII123456\",\"relation\":[1,2],\"minDay\":30,\"maxDay\":90,\"businessType\":3,\"content\":\"逾期天数{MAX_OVERDUE_DAYS}\\n借款姓名{CUST_NAME}\",\"id\":10}";
        SmsUpdateReq req = JSON.parseObject(json, SmsUpdateReq.class);
        Mockito.when(smsService.updateSms(req)).thenReturn(Boolean.TRUE);

        PowerMockito.mockStatic(AsciiCodeUtils.class);
        String result = "逾,期,天,数,借,款,姓,名";
        Mockito.when(AsciiCodeUtils.extractNonASCIICharacters(Mockito.anyString())).thenReturn(result);

        GenericResponse<AddOrUpdateSmsResp> addOrUpdateSmsRespGenericResponse = sysSmsTemplateConfigureBiz.updateSms(req);
        Assert.assertEquals("逾,期,天,数,借,款,姓,名", addOrUpdateSmsRespGenericResponse.getData().getExistNotAsciiCharacter());
    }

    @Test
    public void testGetUserName() throws Exception {
		// mock私有方法是,需要加上@PrepareForTest,并且要用 @Spy 的方式获取对象
        SysSmsTemplateConfigureBiz sysSmsTemplateConfigureBiz = Mockito.spy(SysSmsTemplateConfigureBiz.class);
        MockitoAnnotations.initMocks(this);
        ReflectionTestUtils.setField(sysSmsTemplateConfigureBiz, "authUserBiz", authUserBiz);

        UserBo user = new UserBo();
        user.setName("maoyidong");
        Mockito.when(authUserBiz.getUserById(0L, Boolean.FALSE)).thenReturn(user);

        String result = WhiteboxImpl.invokeMethod(sysSmsTemplateConfigureBiz, "getUserName", 0L);
        Assert.assertEquals("maoyidong", result);
    }
}

我们已经可以对public方法进行mock测试,但此时无法模拟私有方法,静态方法的测试,此时需要用到PowerMockRunner,而不是MockitoJUnitRunner,上面是一个包含了私有方法测试和静态方法测试的例子

空返回测试

@Component
public class DiTingAlarmAdapter implements IAlarmAdapter {

    @Value("${com.ppdai.appId}")
    private String appId;

    @Value("${spring.application.name}")
    private String applicationName;

    @Override
    public Boolean support(Integer alarmPlatform, String alarmChannel) {
        return AlarmPlatformEnum.DI_TING.getCode().equals(alarmPlatform);
    }

    @Override
    public void alarm(AlarmEvent event, AlarmRecord record) {
        String alarmChannels = Arrays.stream(event.getAlarmType().split(",")).map(type -> AlarmTypeEnum.getInstance(Integer.parseInt(type)).getDesc()).collect(Collectors.joining(","));
        AlertDetail alertDetail = new AlertDetail(record.getEventName(), appId, record.getDetail());
        alertDetail.setSeverity("warning")
                .setAlertType("diting.alert.norecovry")
                .setEnv(SpringContextUtil.getEnv())
                .setInstance(applicationName)
                .setAppname(applicationName)
                //"sms"或者"wechat"或者"mail",多个方式,以","分隔
                .setChannels(alarmChannels)
                //接受者:域帐号,多个接收者,以","分隔
                .setReceivers(event.getAlarmTo())
                .setValue("")
                .setCategory("DITING")
                .setSource(AlarmSourceEnum.getInstance(event.getOrigin()).getDesc());

        //告警发出
        AlertManager.sendAlert(alertDetail);
    }
}

如果我们想要调用diTingAlarmAdapter.alarm方法的代码仅跳过,不去做任何操作,可以借助以下代码mock测试

doNothing().when(adapter).alarm(any(), any());
verify(adapter, times(1)).alarm(any(), any());

使用Mockito进行异步测试

下面是使用Mockito进行异步测试的一些常见场景和示例。

模拟异步回调

在异步回调中,当一个异步操作完成时,它将调用一个回调函数来通知调用方。Mockito提供了​​Answe

@Test
public void testAsyncCallback() {
MyAsyncService service = mock(MyAsyncService.class);
when(service.doSomethingAsync(anyString(), any(Consumer.class))).thenAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
String arg1 = (String) args[0];
Consumer<String> callback = (Consumer<String>) args[1];
callback.accept(arg1 + " is done");
return null;
}
});
MyAsyncClient client = new MyAsyncClient(service);
String result = client.doSomething("test");
assertEquals("test is done", result);
}

在这个示例中,我们使用​​Answer​​​接口来模拟异步回调函数。当​​service.doSomethingAsync​​​方法被调用时,我们从参数中获取回调函数并执行它,然后返回​​null​​。在测试中,我们验证异步客户端返回的结果是否正确。

等待异步结果

在异步编程中,我们经常需要等待异步操作完成后获取结果。为了测试异步方法,我们需要等待异步操作完成后再断言结果。Mockito提供了一些方法来处理这种场景。

@Test
public void testAsyncResult() throws Exception {
MyAsyncService service = mock(MyAsyncService.class);
CompletableFuture<String> future = new CompletableFuture<>();
when(service.doSomethingAsync(anyString())).thenReturn(future);
MyAsyncClient client = new MyAsyncClient(service);
CompletableFuture<String> result = client.doSomethingAsync("test");
assertFalse(result.isDone()); // 验证异步方法还未完成
future.complete("test is done");
assertTrue(result.isDone()); // 验证异步方法已完成
assertEquals("test is done", result.get()); // 验证异步方法的结果是否正确
}

在这个示例中,我们使用​​CompletableFuture​​​来模拟异步方法的结果。当​​service.doSomethingAsync​​​方法被调用时,我们返回一个​​CompletableFuture​​​对象。在测试中,我们验证异步方法是否已经启动,然后手动完成​​CompletableFuture​​对象并验证结果是否正确。

需要注意的是,在使用​​CompletableFuture​​​对象进行异步测试时,我们需要等待异步操作完成后再获取结果。我们可以使用​​isDone()​​​方法来判断异步操作是否完成,使用​​get()​​方法来获取异步操作的结果。

总结

Mockito是一个流行的Java模拟框架,它可以帮助开发人员编写单元测试,以便更好地验证代码的正确性。Mockito提供了一些常用的方法,例如模拟对象、测试桩、参数匹配、异步测试等,这些方法可以大大简化测试代码的编写和维护。Mockito的优点包括易学易用、广泛支持、文档丰富等,但也存在局限性,例如不支持mock final方法等。对于开发人员而言,使用Mockito进行单元测试可以提高代码质量,降低代码维护成本,是一个非常值得掌握的技能。

扩展

Junit5

前面的例子都是基于Junit4的,但是目前SpringBoot是默认支持Junit5的
Mockito,Junit5在SpringBoot 内部已依赖只需引入spring-boot-starter-test即可。

<!-- Maven依赖 -->
<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>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <scope>test</scope>
</dependency>

Junit5

Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库
作为最新版本的JUnit框架,JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。
JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。

Junit5基本使用
1、@TestInstance(Lifecycle.PER_CLASS)注解

如果您希望JUnit Jupiter在同一个测试实例上执行所有测试方法,只需使用@TestInstance(Lifecycle.PER_CLASS)注释您的测试类。使用此模式时,每个测试类将创建一个新的测试实例。如果没使用@TestInstance(Lifecycle.PER_CLASS)注解,使用@BeforeAll和@AfterAll注解必须在static静态方法上使用。

2、@ExtendWith(MockitoExtension.class)注解

@ExtendWith注解类似于@RunWith
用在springboot项目中,涉及spring的单元测试需要使用@ExtendWith(SpringExtension.class)注解,可以mock spring bean。不涉及spring时使用@ExtendWith(MockitoExtension.class)。

@ExtendWith(SpringExtension.class)注解在Spring boot 2.1.x需要配合@SpringBootTest 使用,Spring boot 2.1.x之后可以不使用@ExtendWith(SpringExtension.class)注解

3、@SpringBootTest(classes = Application.class)注解

classes = ApplicationStarter.class指向SpringBoot启动类,启动spring容器。

在不同的Spring Boot版本中@ExtendWith的使用:
其中在Spring boot 2.1.x之前:

@SpringBootTest 需要配合@ExtendWith(SpringExtension.class)才能正常工作的。

而在Spring boot 2.1.x之后:

@SpringBootTest 已经组合了@ExtendWith(SpringExtension.class),因此,无需在进行该注解的使用了,进一步简化。如下图@SpringBootTest注解中已包含@ExtendWith(SpringExtension.class)
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4、方法注解

基本的注解都是方法上的注解,意思就是只在测试方法上进行添加,对应注解有以下几种:
这里用Junit4和Junit5做了比较
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

5、断言校验

Assertions.assertEquals()值比较校验:
Assertions.assertThrows()异常捕获校验:
等等其他断言就不做不赘述了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值