本文首先在理论上归纳了单元测试在宏观和微观层面要遵循的基本原则,以及测试覆盖率的要求和评价维度。然后具体阐述了笔者实战中总结的基于Junit + Mockito 的单元测试框架和具体实施方法,并给出了相应的demo代码。
本文主要参考和引用了《码出高效:Java开发手册》,Junit5、mockito等官方文档以及若干篇相关博客的内容,具体可见文末参考链接部分。
由于单元测试只是系统集成测试前的小模块测试,有些因素往往是不具备的,因此需要进行Mock。例如:
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。
该部分以Spring Boot项目为例,介绍单元测试中主要碰到的需求与问题以及相应的实施方法。开发环境选择IntelliJ IDEA 2018。
原则 | DAO层 | service层 | controller层 |
---|---|---|---|
Automatic | 底层测试框架 | ||
Independent/Repeatable | Mock DB | Mock DAO层接口、第三方API | Mock service层接口、Restful请求 |
Border/Error | 参数化测试;重复测试;条件测试;分类测试;模拟数据生成 | ||
Correct | 断言 | ||
Design | 测试报告自动生成 |
<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)))
);
}
org.mockito
mockito-junit-jupiter
RELEASE
test
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);
}
}
@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());
}
}
@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
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