单元测试入门用法

单元测试入门用法

本篇主要介绍在Spring Boot项目中使用JUnit5框架、Mockito框架进行单元测试,使用jacoco统计单元测试代码覆盖率

单元测试做什么

  1. 接口功能测试:保证接口功能正确性,确保接口被正常调用,并输出有效数据
  2. 局部数据结构检测:保证数据结构的正确性
  3. 边界条件测试
  4. 所有独立代码测试:语句覆盖、分支覆盖、条件覆盖、路径覆盖
  5. 异常模块测试:产生异常是否会影响结果

引入依赖

<!-- 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执行一个请求
@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框架

mock静态方法指引

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值