写点什么

JAVA 实战:如何让单元测试覆盖率达到 80% 甚至以上

作者:Java你猿哥
  • 2023-03-22
    湖南
  • 本文字数:7926 字

    阅读完需:约 26 分钟

什么是单元测试?

单元测试(unit testing)是指对软件中的最小可测试单元进行检查和验证。它是软件测试中的一种基本方法,也是软件开发过程中的一个重要步骤。

单元测试的目的是在于确保软件的每个独立模块都被正确地测试,并且没有潜在的缺陷或漏洞。在单元测试中,需要对每个模块进行测试,以确保它们能够按照预期的方式工作,并且没有任何错误或漏洞。

单元测试通常包括以下几个步骤:

  1. 确定测试范围:在开始测试之前,需要确定测试的范围,即要测试的功能或模块。

  2. 编写测试用例:根据确定的测试范围,编写测试用例,这些用例应该覆盖软件中的每个模块。

  3. 执行测试用例:使用测试工具(如 JUnit、TestNG、Mock 等)执行测试用例,以确保每个模块都按照预期的方式工作。

  4. 分析测试结果:在测试完成后,需要分析测试结果,以确定是否存在缺陷或漏洞。

  5. 修复缺陷或漏洞:如果发现缺陷或漏洞,需要修复它们,以确保软件的质量。

单元测试的意义

  • 提高代码质量:通过编写单元测试,可以发现代码中的错误和漏洞,从而提高代码的质量。

  • 提高开发效率:通过编写单元测试,可以快速地发现代码中的问题,从而减少测试时间,提高开发效率。

  • 降低维护成本:通过编写单元测试,可以及早地发现代码中的问题,从而减少维护成本,提高代码的可维护性。

  • 提高代码可靠性:通过编写单元测试,可以检查代码中的错误和漏洞,从而提高代码的可靠性,减少故障的发生。

前言:

看完上面的就知道什么时候或者为什么要编写单元测试了。其他的我们不多说了,直接进入实战操作,这次使用的是 springboot+Mockito 框架,在最后会指出一些小技巧和 bug。

实战

一.Mockito 的 jar 包导入:

<dependencies>  <!-- 单元测试 -->		<dependency>			<groupId>org.jmockit</groupId>			<artifactId>jmockit</artifactId>			<version>1.38</version>			<scope>test</scope>		</dependency>		<dependency>			<groupId>junit</groupId>			<artifactId>junit</artifactId>			<version>4.12</version>			<scope>test</scope>		</dependency>		<dependency>			<groupId>org.powermock</groupId>			<artifactId>powermock-module-junit4</artifactId>			<version>2.0.2</version>			<scope>test</scope>			<exclusions>				<exclusion>					<groupId>junit</groupId>					<artifactId>junit</artifactId>				</exclusion>				<exclusion>					<groupId>org.objenesis</groupId>					<artifactId>objenesis</artifactId>				</exclusion>			</exclusions>		</dependency>		<dependency>			<groupId>org.powermock</groupId>			<artifactId>powermock-api-mockito2</artifactId>			<version>2.0.2</version>			<scope>test</scope>			<exclusions>				<exclusion>					<artifactId>mockito-core</artifactId>					<groupId>org.powermock</groupId>				</exclusion>				<exclusion>					<artifactId>mockito-core</artifactId>					<groupId>org.mockito</groupId>				</exclusion>			</exclusions>		</dependency>
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.9.0</version> <scope>test</scope> </dependency> </dependencies>
复制代码


<build>		<plugins>  <!-- 单元测试 -->			<plugin>				<groupId>org.jacoco</groupId>				<artifactId>jacoco-maven-plugin</artifactId>				<version>0.8.7</version>				<executions>					<execution>						<id>prepare-agent</id>						<goals>							<goal>prepare-agent</goal>						</goals>					</execution>					<execution>						<id>report</id>						<phase>test</phase>						<goals>							<goal>report</goal>						</goals>					</execution>				</executions>			</plugin>			<plugin>				<groupId>org.apache.maven.plugins</groupId>				<artifactId>maven-surefire-plugin</artifactId>				<version>2.12.2</version>				<configuration>					<testFailureIgnore>true</testFailureIgnore>				</configuration>			</plugin>
</plugins> <!-- 修改对应名称 --> <finalName>iot-open-api</finalName> </build>
复制代码


没法上传 pom 文件

二.创建单元测试类

package com.shimao.iot.iotopenapi.service.impl;
import com.shimao.iot.common.bean.AttributesEntity;import com.shimao.iot.common.bean.DeviceDataEntity;import com.shimao.iot.common.bean.DeviceEntity;import com.shimao.iot.common.bean.DeviceTypeEntity;import com.shimao.iot.common.bean.device.UpdateBatchDeviceAttributeReq;import com.shimao.iot.common.bean.member.EditShimaoFaceReq;import com.shimao.iot.common.bean.member.ShimaoFaceReq;import com.shimao.iot.common.elk.entity.DeviceReportEntity;import com.shimao.iot.common.entity.ResultVO;import com.shimao.iot.common.model.device.req.DeviceReportHeartReq;import com.shimao.iot.common.model.device.req.DeviceReportInfoReq;import com.shimao.iot.common.model.face.req.AlarmInfo;import com.shimao.iot.common.model.face.req.DeviceStateReq;import com.shimao.iot.common.model.face.req.FaceCollectInfoReq;import com.shimao.iot.common.model.face.req.FaceCollectReq;import com.shimao.iot.common.model.face.req.PassRecord;import com.shimao.iot.iotopenapi.bean.dto.device.DeviceExtDataEntity;import com.shimao.iot.iotopenapi.kafka.KafkaProducer;import com.shimao.iot.iotopenapi.serviceFeign.DeviceFeignService;import com.shimao.iot.iotopenapi.serviceFeign.ElkClient;import com.shimao.iot.iotopenapi.serviceFeign.MemberClient;import com.shimao.iot.iotopenapi.serviceFeign.OssService;import org.junit.Assert;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.mockito.InjectMocks;import org.mockito.Mock;import org.mockito.Mockito;import org.mockito.MockitoAnnotations;import org.powermock.modules.junit4.PowerMockRunner;import org.springframework.beans.factory.annotation.Value;
import java.util.ArrayList;import java.util.Arrays;import java.util.List;/** * * @author zhangtonghao * @create 2023-01-31 14:41 */@RunWith(PowerMockRunner.class)public class DeviceReportServiceImplTest { @Mock private DeviceFeignService deviceFeignService;
@Mock private OssService ossService;
@InjectMocks com.shimao.iot.iotopenapi.service.Impl.DeviceReportServiceImpl deviceReportServiceImpl;

static { System.setProperty("env", "baseline"); }
@Before public void setUp() { MockitoAnnotations.openMocks(this); }

@Test public void testDeviceLockState() { // Setup DeviceStateReq req = new DeviceStateReq(); req.setEntityCode("entityCode"); req.setGwCode("gwCode"); req.setTimestamp("timestamp"); req.setReqId("reqId"); req.setTypeCode("typeCode"); req.setOpt("opt"); req.setMsgType("msgType"); //存取code AlarmInfo alarmInfo = new AlarmInfo(); alarmInfo.setCode("10000"); alarmInfo.setMessage("message"); alarmInfo.setPictureUrl("pictureUrl"); req.setAlarmInfo(alarmInfo); req.setAttributesEntities(Arrays.asList(new AttributesEntity(0L, 0L, "attributeCode", "value"))); PassRecord passRecord = new PassRecord(); passRecord.setId("id"); passRecord.setRecordId("recordId"); passRecord.setName("name"); passRecord.setPassPhoto("passPhoto"); passRecord.setPassMode("passMode"); passRecord.setResultType(0); passRecord.setPassTime("passTime"); passRecord.setCode("10000"); passRecord.setPersonType(0); req.setPassRecords(Arrays.asList(passRecord));
// Configure DeviceFeignService.queryDeviceInfoByDeviceCode(...). DeviceExtDataEntity deviceExtDataEntity = getDeviceExtDataEntity(); Mockito.when(deviceFeignService.queryDeviceInfoByDeviceCode(Mockito.any())).thenReturn(deviceExtDataEntity); Mockito.when(deviceFeignService.updateAttributesById(Mockito.any())).thenReturn(ResultVO.ok(null)); Mockito.when(ossService.uploadByBase64(Mockito.any())).thenReturn(ResultVO.ok(null));
// Run the test ResultVO result = deviceReportServiceImpl.deviceLockState(req);
// Verify the results Assert.assertNotNull(result); }
private DeviceExtDataEntity getDeviceExtDataEntity() { AttributesEntity attributesEntity = new AttributesEntity(); attributesEntity.setEntityId(11L); attributesEntity.setAttributeCode("11L"); attributesEntity.setValue("11"); List<AttributesEntity> attributes = new ArrayList<>(); attributes.add(attributesEntity);
DeviceExtDataEntity deviceExtDataEntity = new DeviceExtDataEntity(); deviceExtDataEntity.setChannel(1); deviceExtDataEntity.setSpaceId(11L); deviceExtDataEntity.setTypeCode("1"); deviceExtDataEntity.setComdTopic("1"); deviceExtDataEntity.setDeviceCode("11"); deviceExtDataEntity.setDeviceId(11L); deviceExtDataEntity.setDeviceName("11L"); deviceExtDataEntity.setDiff("11"); deviceExtDataEntity.setPanType("11"); deviceExtDataEntity.setSourcePlatform("11"); deviceExtDataEntity.setSpaceId(11L); deviceExtDataEntity.setIconUrl("11"); deviceExtDataEntity.setRootSpaceId(11L); deviceExtDataEntity.setAttributesEntities(attributes); deviceExtDataEntity.setStatus(1); return deviceExtDataEntity; }}
复制代码

三.常用注解了解

简洁版:

  • @InjectMocks:通过创建一个实例,它可以调用真实代码的方法,其余用 @Mock(或 @Spy)注解创建的 mock 将被注入到用该实例中。

  • @Mock:对函数的调用均执行 mock(即虚假函数),不执行真正部分。

  • @Spy:对函数的调用均执行真正部分。(几乎不会使用)

  • Mockito.when( 对象.方法名() ).thenReturn( 自定义结果 ):后面自定返回结果,需要和方法返回结果类型一致,

  • Mockito.any():用于匹配任意类型的参数

详细版:

@RunWith(PowerMockRunner.class)

是 JUnit 的一个 Runner,PowerMockRunner 通过使用 Java Instrumentation API 和字节码操作库 ByteBuddy,使得 Java 类和对象避免了 Java 单继承和 final 类限制,能够进行更灵活的 mock 测试。在 JUnit 中使用 @RunWith(PowerMockRunner.class)来运行单元测试,可以使用 PowerMock 框架进行 Mocking、Stubbing 和 Verification 等操作,它可以完全模拟一个无法模拟的对象,如静态方法、final 类、private 类等。此外,PowerMockRunner 还支持 EasyMock 和 Mockito 等常见的 Mock 技术。

@Mock

所谓的 mock 就是创建一个类的虚假的对象,在测试环境中,用来替换掉真实的对象,以达到两大目的:

  1. 验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等

  2. 指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作

是一个 Mockito 框架中的注解,它可以用于创建一个模拟对象。使用 @Mock 注解可以使测试代码更简洁并且便于阅读,无需手动创建模拟对象。

具体来说,@Mock 注解通常用于测试类中需要测试的类所依赖的对象。当我们使用 @Mock 注解标注一个对象时,这个对象的行为可以被模拟,以便对测试目标类进行测试。在对模拟对象进行测试时,我们可以设定模拟对象的返回值或行为,并用这些值来测试测试目标类的行为。

需要注意的是,使用 @Mock 注解必须先使用 Mockito.mock()初始化 Mock 对象。通常,我们会在测试类的 setUp()方法中使用 @Mock 注解来初始化 Mock 对象,这样测试类的每个测试方法都可以使用它。

同时还需要注意,@Mock 注解只是用于创建一个模拟对象,在使用这个对象进行测试时,需要手动设定其返回值或行为。

@InjectMocks

是 Mockito 框架中的注解。它可以自动为测试类中声明的变量注入被 mock 的对象。使用 @InjectMocks 注解可以让测试代码更加简洁和易读,无需手动创建对象。

具体来说,@InjectMocks 注解通常用于注入一个类的成员变量,这个成员变量通常是另外一个类的实例(被 mock 的对象)。在测试类实例化时,Mockito 会自动查找这个被 mock 对象的实例,然后把它注入到 @InjectMocks 注解标识的变量中。

需要注意的是,@InjectMocks 注解仅仅用于自动注入成员变量。如果需要 mock 类的方法,应该使用 @Mock 注解。

同时,如果一个类里面有多个同类型的成员变量,需要手动使用 @Qualifier 注解来指定需要注入的对象。当然你也可以通过不同名称来区分同一类型的变量。

Mockito.when()

是 Mockito 框架中的一个方法,它可以被用于设定模拟对象的行为。该方法通常和 @Mock 或 @Spy 注解一起使用,用于模拟对象的行为并指定返回值或者其他行为。

具体来说,Mockito.when()方法接受两个参数,一个是模拟对象的方法调用,另一个是指定的行为或返回值。当模拟对象的方法被调用时,Mockito 就会按照 when()方法中指定的方式进行处理。例如,可以使用 Mockito.when()方法来模拟一个方法的返回值.

需要注意的是,Mockito.when()方法并不会真正地执行方法,而是返回了一个指定的返回值或设定的行为,用于在测试中进行验证。同样需要注意的是,如果模拟对象的方法参数不是一个基本类型或 String,则需要手动匹配参数。

Mockito.any()

它可以用于匹配任意类型的参数。在测试代码中,当需要匹配方法的参数但不关心具体的参数值时,可以使用 Mockito.any()方法来匹配参数。

具体来说,Mockito.any()方法可以用于模拟对象的方法调用或验证方法调用时的参数匹配。

需要注意的是,当使用 Mockito.any()方法时,需要确保模拟方法的返回值与模拟方法的参数类型兼容。

常用的 Mockito 方法

Mockito 的使用,一般有以下几种组合:参考链接

  • do/when:包括 doThrow(…).when(…)/doReturn(…).when(…)/doAnswer(…).when(…)

  • given/will:包括 given(…).willReturn(…)/given(…).willAnswer(…)

  • when/then: 包括 when(…).thenReturn(…)/when(…).thenAnswer(…)/when(…).thenThrow(…)

Mockito 的多种匹配函数,部分如下:

四:常见问题

1.我自己明明已经模拟了方法,为什么还无法走通?

mock 中模拟 Mockito.when( 对象.方法名() ).thenReturn( 自定义结果 ),方法名()中参数有的人会使用实际的参数,这样会导致模拟是无法找到正确的结果。所以我们需要使用 Mockito.any()去替代,让 mock 自己去模拟。以及 thenReturn 中返回的值要符合业务逻辑才能保证业务能够走通。参考:

Mockito.when(deviceFeignService.queryDeviceInfoByDeviceCode(Mockito.any())).thenReturn(deviceExtDataEntity);
复制代码

2.为什么有时候使用 Mockito.any()模拟方法时会报错?

这个是因为有时模拟时的参数类型不正确的原因,参考:Mockito 的多种匹配函数。如果还是报错,建议使用准确值,比如参数为 int=1。但就会出现问题一无法返回结果。有知道的大佬可以评论。

3.有时候需要启动参数或者需要连接真实配置(一般 junit 需要同适用)怎么办?

代表启动参数或者是使用的某个配置文件,注解和代码选择其中之一。参考下图

@ActiveProfiles("baseline")

或者

static {

System.setProperty("env", "baseline");

}


4.有的代码中需要判断常量值才能继续往下走,如何模拟?

说实话,这个问题很恶心,麻烦了很久。后来查到可以使用映射测试模拟类,参考:

ReflectionTestUtils.setField()方法接受三个参数:要设置属性值的对象、属性名称和属性值。通过这个方法,我们可以方便地通过反射去设置一个对象的私有变量值,从而在测试代码中控制这个对象的行为。需要注意的是,如果想要通过 ReflectionTestUtils.setField()方法修改的变量是静态的,那么第一个参数应为 null,因为静态变量属于类级别的而不是实例级别的。

ReflectionTestUtils.setField(deviceServiceImpl, "deviceTypeCodes", "1000");
复制代码

5. 代码比较老旧,或者有的需要通过连接 redis 等组件返回结果,业务才能继续往下走?

因为返回的对象无法正常 new,我们可以通过 Mockito.mock()方法可以创建类或接口的模拟对象。比如

// redisTemplate 写法

ListOperations<String, String> listOperations = Mockito.mock(ListOperations.class);

Mockito.when(redisTemplate.opsForList()).thenReturn(listOperations);Mockito.when(listOperations.size(Mockito.any())).thenReturn(10L);

//JDBC 写法

你可以直接带 @Before 方法中去先初始化模拟

@MockDbUtils openCustomDbUtils;
@MockDbUtils newCustomDbUtils;
@InjectMocksNluDataDao test;

@Beforepublic void setUp() { MockitoAnnotations.openMocks(this); getTestByOne();}
private void getTestByOne() { try { Connection conn = Mockito.mock(Connection.class); conn.setAutoCommit(true); PreparedStatement ps = Mockito.mock(PreparedStatement.class); ResultSet rs = Mockito.mock(ResultSet.class); ps.setString(1, "1"); int i = ps.executeUpdate();
PowerMockito.when(conn.prepareStatement(Mockito.any())).thenReturn(ps); PowerMockito.when(ps.getGeneratedKeys()).thenReturn(rs); PowerMockito.when(ps.executeUpdate()).thenReturn(1); PowerMockito.when(openCustomDbUtils.getConn()).thenReturn(conn); } catch (Exception e) {
}
}
@Testpublic void testLoadAllAppVOs() { // Setup getTestByOne(); getTestByFour(); // Run the test test.loadAllAppVOs();}
复制代码

test.loadAllAppVOs()方法代码:


6. 有得使用了一些框架或者工具类去查询数据,比如 mybatiesPlus。代码走不下去怎么办?

其实这也是我为什么讨厌有的人炫技的原因之一。下列报错:


解决方法:


Config config = new Config();EntityHelper.initEntityNameMap(IotStrategyTriggerSensorDO.class,config);

jar 包选择:

import tk.mybatis.mapper.entity.Config;import tk.mybatis.mapper.mapperhelper.EntityHelper;

五:小技巧

有的工程师写完以后想看一下自己覆盖率的多少,以 idea 为例有两种方法。(方法 2 通用)



2.第二种相当于执行 mvn test 命令。有的时候测试报告和 idea 扫描的会有不同,需要以自己环境为准.



idea 插件:Squaretest,帮助自动生成单元测试类。选择第二种使用。


注意:生成后的需要修改,别忘了上面碰到的问题。

创作不易,感觉不错的话请给点个赞吧!我们下期再见!

用户头像

Java你猿哥

关注

一只在编程路上渐行渐远的程序猿 2023-03-09 加入

关注我,了解更多Java、架构、Spring等知识

评论

发布
暂无评论
JAVA实战:如何让单元测试覆盖率达到80%甚至以上_Java_Java你猿哥_InfoQ写作社区