什么是单元测验?

单元测验(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>

JAVA实战:如何让单元测试覆盖率达到80%甚至以上
无法上传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 的多种匹配函数,部分如下:

函数名 匹配类型
any() 所有方针类型
anyInt() 根本类型 int、非 null 的 Integer 类型
anyChar() 根本类型 char、非 null 的 Character 类型
anyShort() 根本类型 short、非 null 的 Short 类型
anyBoolean() 根本类型 boolean、非 null 的 Boolean 类型
anyDouble() 根本类型 double、非 null 的 Double 类型
anyFloat() 根本类型 float、非 null 的 Float 类型
anyLong() 根本类型 long、非 null 的 Long 类型
anyByte() 根本类型 byte、非 null 的 Byte 类型
anyString() String 类型(不能是 null)
anyList() List 类型(不能是 null)
anyMap() Map<K, V>类型(不能是 null)

四:常见问题

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”);

}

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

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办法中去先初始化模仿

@Mock
DbUtils openCustomDbUtils;
@Mock
DbUtils newCustomDbUtils;
@InjectMocks
NluDataDao test;
@Before
public 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) {
    }
}
@Test
public void testLoadAllAppVOs() {
    // Setup
    getTestByOne();
    getTestByFour();
    // Run the test
   test.loadAllAppVOs();
}

test.loadAllAppVOs()办法代码:

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

6. 有得运用了一些结构或许东西类去查询数据,比方mybatiesPlus。代码走不下去怎么办?

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

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

解决办法:

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

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

jar包挑选:

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

五:小技巧

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

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

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

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

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

idea插件:Squaretest,协助主动生成单元测验类。挑选第二种运用。

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

留意:生成后的需求修正,别忘了上面碰到的问题。

创作不易,感觉不错的话请给点个赞吧!我是老白,咱们下期再会!