Java单元测试框架与实践(Junit5 + Mockito)

Java单元测试框架与实践

本文首先在理论上归纳了单元测试在宏观和微观层面要遵循的基本原则,以及测试覆盖率的要求和评价维度。然后具体阐述了笔者实战中总结的基于Junit + Mockito 的单元测试框架和具体实施方法,并给出了相应的demo代码。

本文主要参考和引用了《码出高效:Java开发手册》,Junit5、mockito等官方文档以及若干篇相关博客的内容,具体可见文末参考链接部分。

基本原则

宏观层面:AIR原则

  • A:Automatic(自动化)
    全自动执行,输出结果无需人工检查,而是通过断言验证。
  • I:Independent(独立性)
    分层测试,各层之间不相互依赖。
  • R:Repeatable(可重复)
    可重复执行,不受外部环境( 网络、服务、中间件等)影响。

微观层面:BCDE原则

  • B: Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
  • C: Correct,正确的输入,并得到预期的结果。
  • D: Design,与设计文档相结合,来编写单元测试。
  • E : Error,单元测试的目标是证明程序有错,而不是程序无错。为了发现代码中潜在的错误, 我们需要在编写测试用例时有一些强制的错误输入(如非法数据、异常流程、非业务允许输入等)来得到预期的错误结果。

Mock

由於单元测试只是系统集成测试前的小模块测试,有些因素往往是不具备的,因此需要进行Mock。例如:

  • 功能因素。比如被测试方法内部调用的功能不可用。
  • 时间因素。比如双十一还没有到来,与此时间相关的功能点。
  • 环境因素。政策环境,如支付宝政策类新功能,多端环境, 如PC 、手机等。
  • 数据因素。线下数据样本过小,难以覆盖各种线上真实场景。

覆盖率要求

  • 粗粒度覆盖:类覆盖率和方法覆盖率应达到100%。
  • 细粒度覆盖:行覆盖、分支覆盖、条件判定覆盖、路径覆盖等,相应的覆盖率应考虑上述原则与因素。

测试框架

底层测试框架:junit

Junit主流还是junit4,最新版本是4.12(2014年12月5日),现在最新的是junit5(JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage)。junit5正式版本的发布日期是2017年9月11日,目前最新的版本是5.5.2(2019年9月9日)。我们项目底层选择了junit5。

Mock工具

  • 方法:mockito
    目前,在 Java 阵营中主要的 Mock 测试工具有 Mockito、JMock、EasyMock 等。我们选择了功能更强大且容易上手的Mockito。
    另外,Mockito不支持static的的方法的mock,要使用PowerMock来模拟。但是PowerMock似乎现在还不支持junit5,我们没有使用。
  • 模拟数据生成:jmockdata
  • Redis:redis-mock
  • 数据库:h2
  • 接口:MockMVC

测试实施方法

该部分以Spring Boot项目为例,介绍单元测试中主要碰到的需求与问题以及相应的实施方法。开发环境选择IntelliJ IDEA 2018。

测试需求分析

原则 DAO层 service层 controller层
Automatic 底层测试框架
Independent/Repeatable Mock DB Mock DAO层接口、第三方API Mock service层接口、Restful请求
Border/Error 参数化测试;重复测试;条件测试;分类测试;模拟数据生成
Correct 断言
Design 测试报告自动生成

Junit5的基本使用

Maven导入依赖
<dependency>
   <groupId>org.junit.platform</groupId>
   <artifactId>junit-platform-commons</artifactId>
   <version>1.5.0</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.junit.vintage</groupId>
   <artifactId>junit-vintage-engine</artifactId>
   <version>5.5.2</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.junit.jupiter</groupId>
   <artifactId>junit-jupiter</artifactId>
   <version>5.5.2</version>
   <scope>test</scope>
</dependency>
<dependency>
	<groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.5.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.5.2</version>
    <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.junit.jupiter</groupId>
   <artifactId>junit-jupiter-api</artifactId>
   <version>5.5.2</version>
   <scope>test</scope>
</dependency>
常用注解

在这里插入图片描述

最基本的测试代码
import com.demo.entity.User;
import com.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
public class UserTest {

	@Autowired
	private UserService userService;
	
    @Test
    void userSelectByName(String userName) {
    	User user = userService.selectByName(userName);
        assertNotNull(user);
    }
    
}
参数化测试

使用@ParameterizedTest注解,参数化测试使得测试可以测试多次使用不同的参数值。
参数源:
@ValueSource是最简单的来源之一。它允许你指定单个数组的文字值,并且只能用于为每个参数化的测试调用提供单个参数。
@NullSource注解用来给参数测试提供一个null元素,要求传参的类型不能是基本类型(基本类型不能是null值)
@EmptySource为java.lang.String, java.util.List, java.util.Set, java.util.Map, primitive arrays等类型提供了空参数。
@NullAndEmptySource 注解为上边两个注解的合并。
@EnumSource能够很方便地提供Enum常量。该注解提供了一个可选的names参数,你可以用它来指定使用哪些常量。如果省略了,就意味着所有的常量将被使用。

@SpringBootTest
public class UserTest {

	@Autowired
	private UserService userService;
	
   @ParameterizedTest
   @NullAndEmptySource
   @ValueSource(strings = {"张三", "Thomas", "000001"})
    void userSelectByName(String userName) {
    	User user = userService.selectByName(userName);
        assertNotNull(user);
    }
    
}
重复测试

@RepeatedTest中填入次数可以重复测试。

@SpringBootTest
public class UserTest {

	@Autowired
	private UserService userService;
	
    @RepeatedTest(3)
    void userSelectByName(String userName) {
    	User user = userService.selectByName(userName);
        assertNotNull(user);
    }
    
}
条件测试
//禁用测试
@Disabled("Disabled until bug #99 has been fixed")
void userSelectByName(String userName) {}

//操作系统条件
@EnabledOnOs(MAC)
@DisabledOnOs(WINDOWS)
void userSelectByName(String userName) {}

//脚本条件
@EnabledIf("2 * 3 == 6")
@DisabledIf("Math.random() < 0.314159")
void userSelectByName(String userName) {}

// 基于标签过滤
@Tag("fast")
@Tag("model")
void userSelectByName(String userName) {}
测试执行顺序

可以在类上标注@TestMethodOrder来声明测试方法要有执行顺序,里边可以传入三种类Alphanumeric、OrderAnnotation、Random,分别代表字母排序、数字排序、随机。然后对方法加@Order注解里边传入参数决定顺序。

import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;

@TestMethodOrder(OrderAnnotation.class)
@SpringBootTest
public class UserTest {

	@Autowired
	private UserService userService;
	
    @Test
    @Order(1)
    void userSelectByName(String userName) {
    	User user = userService.selectByName(userName);
        assertNotNull(user);
    }

	@Test
    @Order(2)
    void method2() {}

	@Test
    @Order(3)
    void method3() {}
    
}
断言

所有的JUnit Jupiter断言都是 org.junit.jupiter.api.Assertions类中static方法。可以使用Lambda表达式。
然后如果断言不能满足要求,可以导入第三方的断言库。

动态测试

JUnit Jupiter还引入了一种全新的测试编程模型。
这种新类型的测试是一种动态测试,它是由一个工厂方法在运行时生成的,该方法用@TestFactory注释。与@Test方法相比,@TestFactory方法本身不是测试用例,而是测试用例的工厂。 因此,动态测试是工厂的产物。
同一个@TestFactory所生成的n个动态测试,@BeforeEach和@AfterEach只会在这n个动态测试开始前和结束后各执行一次,不会为每一个单独的动态测试都执行。

	@TestFactory
    Collection<DynamicTest> dynamicTestsFromCollection() {
        return Arrays.asList(
            dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))),
            dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
        );
    }

Mockito的基本使用

Maven导入依赖
<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-junit-jupiter</artifactId>
   <version>RELEASE</version>
   <scope>test</scope>
</dependency>
基于Mockito的service层测试
import com.demo.entity.User;
import com.demo.service.UserService;
import com.demo.dao.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@SpringBootTest
public class UserTest {

	@Mock //将生成MockDao,并注入到@InjectMocks指定的类中
	private UserRepository userRepository;
	
	@InjectMocks //使用Mockito的@InjectMocks注解将待测试的实现类注入
	private UserService userService;
	
    @Test
    void userSelectByName(String userName) {
    	User userMock = new User();
    	userMock.setUserName("张三");
    	when(userRepository.selectByName(any()).thenReturn(userMock);
    	User user = userService.selectByName(userName);
        assertNotNull(user);
    }
    
}
controller层测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserTest {

	@Autowired
	private TestRestTemplate restTemplate;
	
    @Test
    void userSelectByName(String userName) {
    	MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
		headers.add("Content-Type", CONTENT_TYPE_TEST);
		headers.add("Authorization", AUTHORIZATION_TEST);
		ResponseEntity responseEntity = restTemplate.exchange("/user?name=Thomas", HttpMethod.GET, new HttpEntity<Object>(headers), JSONObject.class);
		System.out.println(responseEntity.getBody());
		assertThat(HttpStatus.OK, equalTo(responseEntity.getStatusCode());
    }
    
}
DAO层测试
	@Autowired
	private UserRepository userRepository;
	
	@Test
	@Transactional
	@Rollback(true)// 事务自动回滚,默认是true。可以不写
	public void insertUser(){
		User user = new User();
		assertNotNull(userRepository.insert(user));
	}

模拟数据生成

JmockData 2.0:https://github.com/jsonzou/jmockdata-demo

测试报告生成

命名规范
  • 测试方法命名
    (1)原方法 or 原方法+Test
    (2) test + 待测场景和期待结果的命名方式
    例如,testDecodeUserTokenSuccess
    (3)“should . … When”结构
    例如,shouldSuccessWhenDecodeUserToken

    • 测试类和方法名称呈现
      @DisplayName
覆盖率统计

cobertura:https://www.jianshu.com/p/159880556d6c

报告美化

《JUnit报告美化——ExtentReports》:https://www.cnblogs.com/goldsking/p/7598085.html

参考资料

[1]《码出高效:Java开发手册》:https://book.douban.com/subject/30333948/
[2] 《JUnit 5 User Guide》:https://junit.org/junit5/docs/current/user-guide
[3] JUnit5用户手册:https://www.cnblogs.com/followerofjests/p/10466070.html
[4] 单元测试实践(SpringCloud+Junit5+Mockito+DataMocker):https://www.cnblogs.com/pluto4596/p/11703382.html
[5] Mockito 中文文档 ( 2.0.26 beta ):https://github.com/hehonghui/mockito-doc-zh

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章