一. 基本介绍
junit是Java用户写单元测试用到最多的一种技术,通过一些注解让我们的多个测试用例跑起来,从而检测代码的正确性,这里我们主要介绍一下junit5。
- 用途:Junit一般用来验证独立功能的业务逻辑,比如工具方法等
- 官网地址:【junit5官网】
- 官方文档:【junit5官方文档】
二. api
junit5提供了很多好用的注解,下面只列出最重要的几个注解,如果想看所有注解的话,还是去官网比较好,这里我们只对常用的一些做一下介绍。
1. 常用注解
- @Test:标识只是一个测试用例
- @BeforeEach:在每个测试方法执行执行,总会调用这个方法,一般用于初始化某些数据
- @AfterEach:在每个测试方法执行之后,总会调用这个方法,一般用于释放资源
- @Disabled:忽略测试用例,让相应的测试用例不运行,用在方法上或者类上
- @BeforeAll 和@AfterAll : 和上面的@BeforeEach和@AfterEach非常类似,区别在于,这两个方法必须标注在静态方法上面
2. 高级注解
- @Nested:内嵌测试注解,用于把一组测试归纳起来;
- @RepeatedTest:重复多次测试注解
- @ParameterizedTest:带参数的注解
所有注解请猛戳这里:【Junit5注解】,这些注解,都放在了源码的org.junit.jupiter.api
包下面。
3. 断言Assertions
准备好测试实例、执行了被测类的方法以后,我们需要判断逻辑是否正确,断言用于根据咱们的逻辑来断定会发生什么,确保你得到了想要的结果,Juint5给我们提供了很多的断言方法,这些方法都在org.junit.jupiter.api.Assertions
类中,作用跟方法名一毛一样,一眼就能看出来,如果你不知道咋用,请戳这里:【Junit5断言】
- assert关键字:可以用来断定一些简单的逻辑,如:
assert "hello".length()==5
;个人建议如果有别的可用的时候,先不要用这个,看上去不是很明白具体的意思; - assertEquals:断言结果相等,如果不等,则不通过,有很多对的重载方法;
- assertNotNull:断言不为空,
- assertThrows:断言抛出异常
- assertTimeout:断言超时,如果方法运行的时间超过了指定的时间,就无法通过
- assertAll:进行一组断言,如果前一个失败了,后续不再执行。
4. 假设Assumptions
Assumptions用来做条件测试的,都在org.junit.jupiter.api.Assumptions
包下面,主要有以下几个方法:
- assumeTrue:假设某个事情是正确的,[返回某个字符串]
- assumeFalse:假设某个情况是错误的,[返回某个字符串]
- assumingThat:假设某个表达式是正确时候,执行某个操作
5. 第三方包
junit5集成了一些第三方的包,如:AssertJ, Hamcrest, Truth等,有兴趣的同学可以自行学习。
例如:在junit5中,移除了junit4中的assertThat
断言,我们可以使用Hamcrest Matcher
来进行替代:
import static org.hamcrest.MatcherAssert.assertThat;
....
assertThat(....);
三、使用案例
(一)maven依赖
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
如果是springboot项目,如下引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
(二)基本使用
1. 创建测试类
比如我这里有一个工具类如下:
package com.firewolf.busi.example;
/**
* Hello工具类
*/
public class HelloUtils {
/**
* 打招呼
*
* @param name 人名
* @return
*/
public String sayHello(String name) {
return "hello," + name;
}
/**
* 打招呼,自己传入前缀
*
* @param name 姓名
* @param prefix 前缀
* @return
*/
public String sayHelloWithPrefix(String name, String prefix) {
return prefix + "," + name;
}
}
我们可以自行创建测试用例类,也可以利用Idea的工具来生成测试用例类,我们只需要在所在类的编辑窗口:右键->Generate->Test,就会出现下面的界面:
在这个界面,我们可以自己选择使用的测试类库,所在的包,已经要被测试的方法,我一般只会注意上面的类库是否正确,其他的保持不变;
生成的测试类如下:
package com.firewolf.busi.example;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class HelloUtilsTest {
@BeforeEach
void setUp() {
}
@AfterEach
void tearDown() {
}
@Test
void sayHello() {
}
@Test
void sayHelloWithPrefix() {
}
}
当然,这时候的测试类没有任何测试逻辑
2. 编写测试代码
要测试我们的逻辑是否正确,我们需要自己进行编写,编写过程用到上面提到的api,例如:
package com.firewolf.busi.example;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import java.util.Arrays;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.*;
class HelloUtilsTest {
private HelloUtils helloUtils;
@BeforeEach
void setUp() {
helloUtils = new HelloUtils();
}
@AfterEach
void tearDown() {
helloUtils = null;
}
/**************** 断言 ***************/
@Test
void sayHello() {
assertEquals(helloUtils.sayHello("liuxing"), "hello,liuxing");
}
@Test
void exceptionTest() {
assertThrows(ArithmeticException.class, () -> {
int a = 1 / 0;
});
}
@Test
void timeOutTest() {
assertTimeout(Duration.ofSeconds(1), () -> Thread.sleep(2000));
}
@Test
void sayHelloWithPrefix() {
Exception ex = assertThrows(NullPointerException.class, () -> helloUtils.sayHelloWithPrefix(null, "welcome"));
assertNull(ex.getMessage());
assertAll("helloWithPrefix",
() -> assertEquals(helloUtils.sayHelloWithPrefix("liuxing", "hello"), "hello,liuxing"),
() -> assertThrows(NullPointerException.class, () -> helloUtils.sayHelloWithPrefix(null, "welcome"))
);
}
/************** 第三方jar *****************/
@Test
void testThirdLib() {
assertThat("hello".length(), is(5));
assertThat("hello", isA(String.class));
assertThat(Arrays.asList(1, 2, 3), hasItem(1));
}
/**************** 假设 ***************/
@Test
void testAssumptions() {
assumeTrue("hello".startsWith("h"));
assumeFalse(() -> "hello".endsWith("o"), () -> "hello end with o");
System.setProperty("env", "dev");
assumingThat(System.getProperty("env") != null, () -> {
System.out.println("exec test");
assertEquals(System.getProperty("env").length(), 4);
});
}
}
测试用例需要尽量多的覆盖一些场景,不要只是传入一些常规参数,需要多考虑边界条件,比如,我在sayHelloWithPrefix的测试方法中,传入了null,后面就发现了HelloUtil里面缺少了工具处理。
3. 运行测试用例
有以及几种情况:
- 跑单个测试方法:直接点击方法前面的绿色三角、 右键方法名->debug/run
- 跑单个测试类:点击类上面的绿色三角 、右键类的空白处->debug/run、 右键方法名->debug/run
- 运行某个包下面的测试用例:右键包名-> debug/run
- 运行所有测试用例:右键test下面的Java文件夹、点击maven生命周期的test、进入项目根目录->mvn test
run和debug的区别在于debug的话会进行调试,一般我们会在出错之后这么去找错误;
4. 查看测试结果
- 某个类或者某个方法的测试结果:我们可以通过idea的运行结果来查看我们的代码是否达到了我们想要的目标,如:
只有方法前面标识了绿色对勾的时候,才是正确的。 - 整个工程的测试通过情况
当我们使用mvn test 或者用maven插件执行的时候,可以明确看到那些测试用例报错了
5. 跳过测试用例
我们的项目终究是要以jar或者其他的形式提供出去的,这个步骤对应着maven生命周期的deploy,而maven生命周期中,test位于depoy之前,也就是说,在我们deploy的时候,会先跑测试用例,如果测试用例耗时较多,那么这个过程会比较慢,此时我们可以通过以下几种方式跳过测试用例:
- . 跳过全部测试用例
- maven命令后面加上-Dmaven.test.skip=true,如:mvn clean install -Dmaven.test.skip=true
- maven插件添加如下配置:
<configuration> <skip>true</skip> </configuration>
- 跳过部分测试用例
- 在类或者非方法上面添加@Disabled
(三)重复执行
有时候我们需要一个方法多执行几次才能达到我们测试的目的,比如定时任务等。
我们可以使用@RepeatedTest来完成这个功能
1. 注解属性
- value:重复次数,必填
- name:这次执行的名字,里面可以用到三个定义好的占位符:
- {displayName}:测试的展示名,如果方法上面使用了@DisplayName注解的话,就会是这个注解里面的值,否则显示为方法名
- {currentRepetition} :当前执行的次数
- {totalRepetitions}:共次数
2. 示例代码
package com.firewolf.busi.example;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.TestInfo;
class RepeatTestDriver {
@DisplayName("repeat test")
@RepeatedTest(value = 4, name = "执行测试: {displayName}, 第 {currentRepetition} / {totalRepetitions} 次! ")
void testRepeat(TestInfo testInfo) {
assert !testInfo.getDisplayName().contains("3");
}
}
(四)传递参数
有时候我们希望给测试用例传入我们需要的参数,这个时候,我们可以使用下面的一系列注解来完成这个事情注解来完成这个需求。
1. @ParameterizedTest
作用:标注这是一个带参数的单元测试;
参数:
- name:每一个参数对应的测试方法名字,可以使用如下占位符:{index}(第Index个参数)、{arguments}(当前这组参数),{0}/{1}/…(这组参数的第几个参数)、{displayName}:方法名
这个注解需要配合下面的一堆注解一起使用
2. @ValueSource
作用:传入一组参数
参数:
- ints:传入一组整数
- …:这些参数和ints类似,可以传入八种基本数据类型、java.lang.String类型、java.lang.Class类型。
方式为:ints={},strings={} 等等;
示例:
@ParameterizedTest(name = "第 {index}个参数, 当前参数:{arguments}")
@ValueSource(ints = {1, 2, 3})
void testValueSourceIntParams(int param) {
assertTrue(param > 0 && param < 4);
}
3. @NullSource
作用:传入空值null
4. @EmptySource
作用:传入空数据
如:java.lang.String, java.util.List, java.util.Set, java.util.Map, primitive arrays (e.g., int[], char[][], etc.), object arrays (e.g.,String[], Integer[][], etc.)
.
5. @NullAndEmptySource
作用:@NullSource和@EmptySource这两个注解的组合;
示例:
@ParameterizedTest
// 传入三个字符串
@ValueSource(strings = {"haha", "hehe", "heihei"})
// 传入null和""
@NullAndEmptySource
void testValueSourceStringParams(String str) {
assertEquals(str.length(), 4);
}
6. @EnumSource
作用:传入枚举中的值
参数:
- value:枚举类型
- names:关心的枚举集合。
- mode:对names的处理方式,默认是INCLUDE,也就是传入names指定的枚举,也可以使用EXCLUDE来排除指定的枚举;
要求:
需要引入下面的依:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
示例:
@ParameterizedTest
@EnumSource(value = ChronoUnit.class, names = {"SECONDS", "DAYS"}, mode = EnumSource.Mode.INCLUDE)
void testEumSource(ChronoUnit unit) {
assertNotNull(unit);
}
7. @MethodSource
作用:通过方法来传入参数
参数:
- value:提供参数方法名,如果方法是当前类的,就直接给方法名,如果是其他类的,需要使用
类的包名#方法名
形式传入,如:com.firewolf.busi.example.ParamTestDriver#provider
要求:方法必须是静态类型,且方法返回的必须是有个Stream类型,如:IntStream、Stream等等;
示例:
@ParameterizedTest
@MethodSource("com.firewolf.busi.example.ParamTestDriver#provider")
void testMethodParams(ChronoUnit chronoUnit) {
System.out.println(chronoUnit);
}
static Stream<ChronoUnit> provider() {
return Stream.of(ChronoUnit.HALF_DAYS, ChronoUnit.DAYS);
}
8. @CsvSource
作用:通过Csv格式传入一组参数
参数:
- value:参数
- delimiter:各个值之间的分隔符,默认为’,’
注意点:
- 多个单词的参数:如果某项数据里面包含了分隔符,那么需要使用’'括起来;
- 传入null:需要想传入null,那么这一项不写即可;
- 如果数据项多于参数,那么后面的会被丢弃
示例:
@ParameterizedTest
@CsvSource(value = {
"apple ; 2; heihei",
" ; 1; heihei",
"'lemon; lime'; 2; haha",
"nal; 0xF1; hehe"
}, delimiter = ';')
void testWithCsvSource(String fruit, int rank) {
System.out.println(fruit);
assertNotNull(fruit);
assertNotEquals(0, rank);
}
9. @CsvFileSource
作用:通过csv文件注入参数
参数:
- value:参数
- delimiter:各个值之间的分隔符,默认为’,’
- resources:文件,这个文件需要放在
src/test/resources/
下面,然后文件路径以/文件名
的形式 - lineSeparato:换行符,默认为
\n
- encoding:文件编码,默认为utf-8
注意事项:如果文件中的某项数据包含了分隔符,那么需要使用""来引用起来
示例:
@ParameterizedTest
@CsvFileSource(resources = "/test.csv", delimiter = ';')
void testWithCsvFileSource(String fruit, int rank) {
System.out.println(fruit);
assertNotNull(fruit);
assertNotEquals(0, rank);
}
10. ArgumentsAccessor
我们可以把传入的参数转成我们需要的类型,然后作为测试用例的参数传入方法。主要有两种方式:
10.1 ArgumentsAccessor
给测试用例传入一个ArgumentsAccessor类型的参数,然后在方法里面进行封装,
如:
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
Person person = new Person(arguments.getString(0),
arguments.getString(1),
arguments.get(2, String.class),
arguments.get(3, LocalDate.class));
if (person.getFirstName().equals("Jane")) {
assertEquals("F", person.getGender());
} else {
assertEquals("M", person.getGender());
}
assertEquals("Doe", person.getLastName());
assertEquals(1990, person.getDateOfBirth().getYear());
}
10.2 自定义 ArgumentsAccessor
我们可以实现自己的ArgumentsAccessor,这个类需要实现接口ArgumentsAggregator ;然后使用@AggregateWith注解来指定我们自己定义的转换器,
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithArgumentsAccessor2(@AggregateWith(PersonArgumentsAccessor.class) Person person) {
if (person.getFirstName().equals("Jane")) {
assertEquals("F", person.getGender());
} else {
assertEquals("M", person.getGender());
}
assertEquals("Doe", person.getLastName());
assertEquals(1990, person.getDateOfBirth().getYear());
}
class PersonArgumentsAccessor implements ArgumentsAggregator {
@Override
public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext parameterContext) throws ArgumentsAggregationException {
Person person = new Person(arguments.getString(0),
arguments.getString(1),
arguments.get(2, String.class),
arguments.get(3, LocalDate.class));
return person;
}
}