junit是Java用户写单元测试用到最多的一种技术,通过一些注解让我们的多个测试用例跑起来,从而检测代码的正确性,这里我们主要介绍一下junit5。
junit5提供了很多好用的注解,下面只列出最重要的几个注解,如果想看所有注解的话,还是去官网比较好,这里我们只对常用的一些做一下介绍。
所有注解请猛戳这里:【Junit5注解】,这些注解,都放在了源码的org.junit.jupiter.api
包下面。
准备好测试实例、执行了被测类的方法以后,我们需要判断逻辑是否正确,断言用于根据咱们的逻辑来断定会发生什么,确保你得到了想要的结果,Juint5给我们提供了很多的断言方法,这些方法都在org.junit.jupiter.api.Assertions
类中,作用跟方法名一毛一样,一眼就能看出来,如果你不知道咋用,请戳这里:【Junit5断言】
assert "hello".length()==5
;个人建议如果有别的可用的时候,先不要用这个,看上去不是很明白具体的意思;Assumptions用来做条件测试的,都在org.junit.jupiter.api.Assumptions
包下面,主要有以下几个方法:
junit5集成了一些第三方的包,如:AssertJ, Hamcrest, Truth等,有兴趣的同学可以自行学习。
例如:在junit5中,移除了junit4中的assertThat
断言,我们可以使用Hamcrest Matcher
来进行替代:
import static org.hamcrest.MatcherAssert.assertThat;
....
assertThat(....);
<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>
比如我这里有一个工具类如下:
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() {
}
}
当然,这时候的测试类没有任何测试逻辑
要测试我们的逻辑是否正确,我们需要自己进行编写,编写过程用到上面提到的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里面缺少了工具处理。
有以及几种情况:
我们的项目终究是要以jar或者其他的形式提供出去的,这个步骤对应着maven生命周期的deploy,而maven生命周期中,test位于depoy之前,也就是说,在我们deploy的时候,会先跑测试用例,如果测试用例耗时较多,那么这个过程会比较慢,此时我们可以通过以下几种方式跳过测试用例:
<configuration>
<skip>true</skip>
</configuration>
有时候我们需要一个方法多执行几次才能达到我们测试的目的,比如定时任务等。
我们可以使用@RepeatedTest来完成这个功能
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");
}
}
有时候我们希望给测试用例传入我们需要的参数,这个时候,我们可以使用下面的一系列注解来完成这个事情注解来完成这个需求。
作用:标注这是一个带参数的单元测试;
参数:
这个注解需要配合下面的一堆注解一起使用
作用:传入一组参数
参数:
示例:
@ParameterizedTest(name = "第 {index}个参数, 当前参数:{arguments}")
@ValueSource(ints = {1, 2, 3})
void testValueSourceIntParams(int param) {
assertTrue(param > 0 && param < 4);
}
作用:传入空值null
作用:传入空数据
如: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.)
.
作用:@NullSource和@EmptySource这两个注解的组合;
示例:
@ParameterizedTest
// 传入三个字符串
@ValueSource(strings = {"haha", "hehe", "heihei"})
// 传入null和""
@NullAndEmptySource
void testValueSourceStringParams(String str) {
assertEquals(str.length(), 4);
}
作用:传入枚举中的值
参数:
要求:
需要引入下面的依:
<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);
}
作用:通过方法来传入参数
参数:
类的包名#方法名
形式传入,如: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);
}
作用:通过Csv格式传入一组参数
参数:
注意点:
示例:
@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);
}
作用:通过csv文件注入参数
参数:
src/test/resources/
下面,然后文件路径以/文件名
的形式\n
注意事项:如果文件中的某项数据包含了分隔符,那么需要使用""来引用起来
示例:
@ParameterizedTest
@CsvFileSource(resources = "/test.csv", delimiter = ';')
void testWithCsvFileSource(String fruit, int rank) {
System.out.println(fruit);
assertNotNull(fruit);
assertNotEquals(0, rank);
}
我们可以把传入的参数转成我们需要的类型,然后作为测试用例的参数传入方法。主要有两种方式:
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;
}
}