单元测试入门用法
本篇主要介绍在Spring Boot
项目中使用JUnit5
框架、Mockito
框架进行单元测试,使用jacoco
统计单元测试代码覆盖率
单元测试做什么
- 接口功能测试:保证接口功能正确性,确保接口被正常调用,并输出有效数据
- 局部数据结构检测:保证数据结构的正确性
- 边界条件测试
- 所有独立代码测试:语句覆盖、分支覆盖、条件覆盖、路径覆盖
- 异常模块测试:产生异常是否会影响结果
引入依赖
<!-- jacoco版本 -->
<properties>
<jacoco.version>0.8.5</jacoco.version>
</properties>
<!-- 导入test包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion><!-- 防止使用旧的junit4相关接口我们将其依赖排除 -->
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- jacoco -->
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<scope>test</scope>
</dependency>
<!-- 为使用mockito的最新特性,导入mockito相关最新版本的包 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.6.28</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.6.28</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>3.4.0</version>
<scope>test</scope>
</dependency>
<!--添加jacoco允许的插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M3</version>
<configuration>
<argLine>
-javaagent:${settings.localRepository}/org/jacoco/org.jacoco.agent/${jacoco.version}/org.jacoco.agent-${jacoco.version}-runtime.jar=destfile=${project.basedir}/target/coverage-reports/jacoco-unit.exec
</argLine>
<testFailureIgnore>true</testFailureIgnore>
</configuration>
</plugin>
<!--检查代码覆盖率的插件配置-->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<configuration>
<!--指定生成.exec文件的存放位置-->
<destFile>target/coverage-reports/jacoco-unit.exec</destFile>
<!--Jacoco是根据.exec文件生成最终的报告,所以需指定.exec的存放路径-->
<dataFile>target/coverage-reports/jacoco-unit.exec</dataFile>
<!--排除掉的代码路径-->
<excludes>
<exclude>**/dao/**</exclude>
<exclude>**/dto/**</exclude>
<exclude>**/entity/**</exclude>
<exclude>**/dao/**</exclude>
<exclude>**/vo/**</exclude>
<exclude>**/prometheus/**</exclude>
<exclude>**/sharding/**</exclude>
<exclude>**/zookeeper/**</exclude>
<exclude>**/config/**</exclude>
<exclude>**/exception/**</exclude>
<exclude>**/job/**</exclude>
<exclude>**/lock/**</exclude>
<exclude>**/recording/**</exclude>
<exclude>**/controller/req/**</exclude>
<exclude>**/constant/**</exclude>
<exclude>**/validator/**</exclude>
<exclude>**/aop/**</exclude>
</excludes>
<!-- rules里面指定覆盖规则 -->
<rules>
<rule implementation="org.jacoco.maven.RuleConfiguration">
<element>BUNDLE</element>
<limits>
<!-- 指定方法覆盖到50% -->
<limit implementation="org.jacoco.report.check.Limit">
<counter>METHOD</counter>
<value>COVEREDRATIO</value>
<minimum>0.50</minimum>
</limit>
<!-- 指定分支覆盖到50% -->
<limit implementation="org.jacoco.report.check.Limit">
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.50</minimum>
</limit>
<!-- 指定类覆盖到100%,不能遗失任何类 -->
<limit implementation="org.jacoco.report.check.Limit">
<counter>CLASS</counter>
<value>MISSEDCOUNT</value>
<maximum>0</maximum>
</limit>
</limits>
</rule>
</rules>
</configuration>
<executions>
<execution>
<id>pre-test</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>post-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
Controller类单元测试
-
使用MockMvc进行参数校验,MockMvc是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用
-
Controller类
@RestController
@RequestMapping("/index")
@Validated
public class IndexController {
@Autowired
private ProductService productService;
@PostMapping("/v1/product/list")
public Result<ProductListVO> queryProductList(@Valid @RequestBody ProductListReq productListReq) {
ProductListDTO productListDTO = productService.queryProductList(productListReq);
return Result.success(ProductListVO.build(productListDTO));
}
}
- 请求参数实体类
@Data
public class ProductListReq {
@Digits(integer = 5, fraction = 0, message = "pageSize is illegal")
private int pageSize = 10;
@Digits(integer = 5, fraction = 0, message = "pageNum is illegal")
private int pageNum = 1;
@NotBlank(message = "sortField can’t be blank")
private String sortField;
}
- 编写测试类
- 1.实例化
MockMvc
实体 - 2.使用mockMvc.perform执行一个请求
- 1.实例化
@ExtendWith({MockitoExtension.class})
public class IndexControllerTest {
@InjectMocks
private IndexController indexController;
@Mock
private ProductServiceImpl productService;
public void IndexControllerTest() {
MockitoAnnotations.openMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(indexController).build();
}
@Test
void queryProductList() throws Exception {
Mockito.when(productService.queryProductList(Mockito.any(ProductListReq.class))).thenReturn(Arrays.asList());
ProductListReq productListReq = new ProductListReq();
productListReq.setSortField("modifyDate");
mockMvc.perform(post("/index/v1/product/list").content(JSON.toJSONString(productListReq))
.contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk())
.andDo(print()).andReturn().getResponse().getContentAsString();
}
}
生成jacoco代码覆盖率报告
maven命令
mvn clean test org.jacoco:jacoco-maven-plugin:0.8.5:prepare-agent
补充
@Value注入Mock
- Service中使用@Value注入变量
public class ProductServiceImpl implement ProductService {
// 注入变量
@Value("${kf.get.clients.endpoint}")
private String getClientEndpoint;
}
- Mock中使用
ReflectionTestUtils.setField()
设置变量值
ReflectionTestUtils.setField(productServiceImpl, "getClientEndpoint", "lorem");
ReflectionTestUtils.setField()
:将targetObject上具有给定名称的字段设置为value值。【更多用法:https://blog.csdn.net/zhuqiuhui/article/details/88215632】
@InjectMocks与@Mock的区别
@Mock
creates a mock.
@InjectMocks
creates an instance of the class and injects the mocks that are created with the@Mock
(or@Spy
) annotations into this instance.
@InjectMocks
: 要mock的组件(controller、service、component)
@Mock / @MockBean
:mock的Bean对象,会被注入到@InjectMocks
标注的组件中,等价于@Autowired
@ExtendWith(SpringExtension.class)与@ExtendWith(MockitoExtension.class)的区别
- When involving Spring:
If you want to use Spring test framework features in your tests like for example @MockBean
, then you have to use @ExtendWith(SpringExtension.class)
. It replaces the deprecated JUnit4 @RunWith(SpringJUnit4ClassRunner.class)
- When NOT involving Spring:
If you just want to involve Mockito and don’t have to involve Spring, for example, when you just want to use the @Mock
/ @InjectMocks
annotations, then you want to use @ExtendWith(MockitoExtension.class)
, as it doesn’t load in a bunch of unneeded Spring stuff. It replaces the deprecated JUnit4 @RunWith(MockitoJUnitRunner.class)
.
- To answer your question:
Yes you can just use @ExtendWith(SpringExtension.class)
, but if you’re not involving Spring test framework features in your tests, then you probably want to just use @ExtendWith(MockitoExtension.class)
.
- 简单的说,这两个注解是类似的功能,区别只在于引用的依赖不一样:
@ExtendWith(SpringExtension.class)
和@MockBean
适用于Spring框架@ExtendWith(MockitoExtension.class)
和@Mock
/@InjectMocks
适用于非Spring框架