JUnit5 介绍
什么是 xUnit ?
Java 语⾔的 xUnit 主流框架:
什么是 JUnit5 ?
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
- JUnit Platform::用于 JVM 上启动测试框架的基础服务,提供命令行,IDE和构建工具等方式执行测试的支持。不仅支持 Junit 自制的测试引擎,其他测试引擎也都可以接入。
- JUnit Jupiter:JUnit Jupiter 提供了 JUnit5 的新的编程模型和扩展模型,是 JUnit5 新特性的核心。内部包含了一个测试引擎,用于在 Junit Platform 上运行。
- JUnit Vintage:由于 JUint 已经发展多年,为了照顾老的项目,JUnit Vintage 提供了兼容 JUnit4.x、Junit3.x 的测试引擎。
通过上述的介绍,不知道有没有发现 JUint5 似乎已经不再满足于安安静静做一个单元测试框架了,它的野心很大,想通过接入不同测试引擎,来支持各类测试框架的使用,成为一个基于 JVM 测试框架的基础平台。因此它也采用了分层的架构,分为了平台层、引擎层、框架层。下图可以很清晰地体现出来:
-
最核心的就是平台层:IDE 和构建工具都是作为客户端和这个平台层交互,以达到在项目中运行测试的目的。TestEngine 的实现在平台层中用于发现和运行测试,并且输出测试报告,并通过平台层返回给客户端。
-
核心关注点是扩展能力:不仅仅只是存在于测试类级别,在整个测试平台级别,都提供了足够的扩展能力。只需要实现框架本身对 TestEngine 的接口,任何测试框架都可以在 JUnit Platform 上运行,这代表着 JUnit5 将会有着很强的拓展性。只需要一点点工作,通过这一个扩展点,框架就能得到所有 IDE 和构建工具在测试上的支持。这对于新框架来说绝对是好事,在测试和构建这块的门槛更低。如 JUnit Vintage 就是一个 TestEngine 实现,用于执行 JUnit4 的测试。
-
这些对于一个开发者来说意味着什么呢?这意味着一个测试框架和 JVM 开发市场上所有主流的工具集成的时候,你能更容易地说服你的经理、开发 leader、任何项阻碍你引入这个测试框架的人。
Junit5 新特性
JUnit5 更像是 JUnit4 的一个超集,他提供了非常多的增强:
- JUnit5 不再是单个库,而是模块化结构的集合。整个 API 分成了:自己的模块、引擎、Launcher、针对 Gradle 和 Surefire 的集成模块。
- 更强大的断言功能和测试注解。
- 嵌套测试类:不仅仅是 BDD(Behavior Driven Development)。
- 动态测试:在运行时生成测试用例。
- 扩展测试:JUnit5 提供了很多的标准扩展接口,第三方可以直接实现这些接口来提供自定义的行为。通过 @ExtendWith 注解可以声明在测试方法和类的执行中启用相应的扩展。
- 支持 Hamcrest 匹配和 AssertJ 断言库,可以用它们来代替 JUnit5 的方法。
- 实现了模块化,让测试执行和测试发现等不同模块解耦,减少依赖。
- 提供对 Java8 的支持,如 Sream API、Lambda 表达式(允许你通过表达式来代替功能接口)等。
- 提供了分组断言(允许执行一组断言,且会一起报告)。
迁移指南:
JUnit 平台可以通过 Jupiter 引擎来运行 JUnit 5 测试,通过 Vintage 引擎来运行 JUnit 3 和 JUnit 4 测试。因此,已有的 JUnit 3 和 4 的测试不需要任何修改就可以直接在 JUnit 平台上运行。只需要确保 Vintage 引擎的 jar 包出现在 classpath 中,JUnit 平台会自动发现并使用该引擎来运行 JUnit 3 和 4 测试。
开发人员可以按照自己的项目安排来规划迁移到 JUnit 5 的进度。可以保持已有的 JUnit 3 和 4 的测试用例不变,而新增加的测试用例则使用 JUnit 5。
在进行迁移的时候需要注意如下的变化:
- 注解在 org.junit.jupiter.api 包中;断言在 org.junit.jupiter.api.Assertions 类中;前置条件在 org.junit.jupiter.api.Assumptions 类中
- 把 @Before 和 @After 替换成 @BeforeEach 和 @AfterEach
- 把 @BeforeClass 和 @AfterClass 替换成 @BeforeAll 和 @AfterAll
- 把 @Ignore 替换成 @Disabled
- 把 @Category 替换成 @Tag
- 把 @RunWith、@Rule 和 @ClassRule 替换成 @ExtendWith
Junit5 注解
注解 | 说明 |
---|---|
@Test | 表示方法是测试方法(与 JUnit4 的 @Test 不同,它的职责非常单一,不能声明任何属性,拓展的测试将会由 Jupiter 提供额外注解) |
@ParameterizedTest | 表示方法是参数化测试 |
@RepeatedTest | 表示方法可重复执行 |
@DisplayName | 为测试类或者测试方法设置展示名称 |
@BeforeEach | 表示在每个测试方法之前执行 |
@AfterEach | 表示在每个测试方法之后执行 |
@BeforeAll | 只执行一次,执行时机是在所有测试方法和 @BeforeEach 注解方法之前 |
@AfterAll | 只执行一次,执行时机是在所有测试方法和 @AfterEach 注解方法之后 |
@Tag | 表示单元测试类别。类似于 JUnit4 中的 @Categories |
@Disabled | 表示测试类或测试方法不执行。类似于 JUnit4 中的 @Ignore |
@Timeout | 表示测试方法运行如果超过了指定时间将会返回错误 |
@ExtendWith | 为测试类或测试方法提供扩展类引用 |
- JUnit5 不再需要手动将测试类与测试方法为 public,包可见的访问级别就足够了。
- 因为框架会为每个测试类创建一个单独的实例,且在 @BeforeAll/@AfterAll 方法执行时,尚无任何测试实例诞生。因此,这两个方法必须定义为静态方法。
示例:
- Maven 依赖:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
- 测试代码:
import org.junit.jupiter.api.*;
class StandardTests {
@BeforeAll
static void initAll() {
System.out.println("BeforeAll");
}
@BeforeEach
void init() {
System.out.println("BeforeEach");
}
@Test
void test1() {
System.out.println("test1");
}
@Test
void test2() {
System.out.println("test2");
}
@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
System.out.println("skippedTest");
}
@AfterEach
void tearDown() {
System.out.println("AfterEach");
}
@AfterAll
static void tearDownAll() {
System.out.println("AfterAll");
}
}
执行结果:
BeforeAll
BeforeEach
test1
AfterEach
BeforeEach
test2
AfterEach
for demonstration purposes
AfterAll
断言
JUnit5 使用了新的断言类:org.junit.jupiter.api.Assertions。相比之前的 Assert 断言类多了许多新的功能,并且大量方法支持 Java8 的 Lambda 表达式。
JUnit5 常用断言方法:
方法 | 说明 |
---|---|
assertEquals(expected, actual) | 查看两个对象是否相等。 (类似于字符串比较使用的 equals() 方法) |
assertNotEquals(first, second) | 查看两个对象是否不相等。 |
assertNull(object) | 查看对象是否为空。 |
assertNotNull(object) | 查看对象是否不为空。 |
assertSame(expected, actual) | 查看两个对象的引用是否相等。 (类似于使用“==”比较两个对象) |
assertNotSame(unexpected, actual) | 查看两个对象的引用是否不相等。 (类似于使用“!=”比较两个对象) |
assertTrue(condition) | 查看运行结果是否为 true。 |
assertFalse(condition) | 查看运行结果是否为 false。 |
assertArrayEquals(expecteds, actuals) | 查看两个数组是否相等。 |
assertThat(actual, matcher) | 查看实际值是否满足指定的条件。 |
fail() | 让测试执行失败。 |
以下为两个与 JUnit4 不太一样的断言方式。
异常断言
JUnit5 提供了一种新的异常断言方式 Assertions.assertThrows(),配合函数式编程就可以进行使用。
我们先来考虑一下下面这个 JUnit4 测试:
@Test(expected = IllegalArgumentException.class)
public void shouldThrowException() throws Exception {
Task task = buildTask();
LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
task.execute(oneHourAgo);
}
想象我们运行这个测试,如果传入到 execute() 方法中的参数是一个过去的时间,会正常抛出一个 IllegalArgumentException 异常。这种情况下,测试会运行通过。
但是如果在 buildTask() 方法中抛出了一个其他类型的异常呢?测试会正常执行,并且会提示你得到的异常和期望异常不匹配。这里问题就出来了,我们只是希望测试是在指定位置得到指定的异常,而不是在整个测试体中出现的异常都作为对比异常。
为此,在 JUnit5 中,提供了一个 assertThrows() 方法,可以非常轻松地处理这个问题:
@Test
void shouldThrowException() throws Exception {
Task task = buildTask();
LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
assertThrows(IllegalArgumentException.class,
() -> task.execute(oneHourAgo));
}
超时断言
同样的,Junit5 还提供了 Assertions.assertTimeout() 方法,为测试方法的指定位置,设置超时测试。
在这种情况下,就不会担心测试的 setup 阶段对代码执行时间的影响,你可以指定只去衡量某一段代码的执行时间。
另外还提供了一个选项:当出现超时的时候,是选择停止执行(即断言失败)还是继续当前测试(以衡量代码执行的真实完整时间)。
@Test
void shouldTimeout() throws Exception {
ExpensiveService service = setupService();
assertTimeout(ofSeconds(3), () -> {
service.expensiveMethod();
});
}
AssertAll(软断言)
问题现象:有⼀个⽅法存在多个断⾔,但是其中⼀个断⾔失败了,后⾯的断⾔都没有执⾏,难道我要等第⼀个问题修好了才能继续检查后⾯的断⾔么?
问题原因:因为原来使⽤的是 JUnit5 的普通断⾔,当⼀个断⾔失败会直接跳出测试⽅法,导致后⾯的断⾔⽆法执⾏,此时的脚本容错性较低。
解决思路:
- 拆开多个测试⽅法,每个测试⽅法进⾏⼀个断⾔。(会造成⼤量重复代码,此⽅案被否)
- 使⽤软断⾔,即使⼀个断⾔失败,仍会进⾏进⾏余下的断⾔,然后统⼀输出所有断⾔结果。
实施⽅案:可以使⽤ JUnit5 提供的 Java8lambdas
的断⾔⽅法,当⼀个断⾔失败,剩下的断⾔依然会执⾏,脚本的容错性增强。
Junit5 带来新的断⾔⽅式:assertAll
断⾔⽅法,会在执⾏完所有断⾔后统⼀输出结果,⼀次性暴露所有问题,提⾼了测试脚本的健壮性。
@Test
public void fun() {
// 组内有一个断言方法不通过,则整组断言结果也不通过
assertAll("断言描述1",
() -> assertEquals(1, 2), // 断言失败
() -> System.out.println("测试打印") // 即使上一行断言失败,仍能执行该行代码
);
// 若上组断言不通过,则该组断言不执行
assertAll("断言描述2",
() -> assertEquals(1, 2),
() -> assertEquals(2, 2)
);
}
运行结果:
测试打印
expected: <1> but was: <2>
Comparison Failure:
Expected :1
Actual :2
<Click to see difference>
org.opentest4j.MultipleFailuresError: 断言描述1 (1 failure)
org.opentest4j.AssertionFailedError: expected: <1> but was: <2>
.....
综合示例
class AssertionsDemo {
@Test
void standardAssertions() {
assertEquals(2, 2);
assertEquals(4, 4, "The optional assertion message is now the last parameter.");
assertTrue(2 == 2, () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
}
@Test
void groupedAssertions() {
// In a grouped assertion all assertions are executed, and any
// failures will be reported together.
assertAll("person",
() -> assertEquals("John", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
}
@Test
void dependentAssertions() {
// Within a code block, if an assertion fails the
// subsequent code in the same block will be skipped.
assertAll("properties",
() -> {
String firstName = person.getFirstName();
assertNotNull(firstName);
// Executed only if the previous assertion is valid.
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("n"))
);
},
() -> {
// Grouped assertion, so processed independently
// of results of first name assertions.
String lastName = person.getLastName();
assertNotNull(lastName);
// Executed only if the previous assertion is valid.
assertAll("last name",
() -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e"))
);
}
);
}
@Test
void exceptionTesting() {
Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("a message");
});
assertEquals("a message", exception.getMessage());
}
@Test
void timeoutNotExceeded() {
// The following assertion succeeds.
assertTimeout(ofMinutes(2), () -> {
// Perform task that takes less than 2 minutes.
});
}
@Test
void timeoutNotExceededWithResult() {
// The following assertion succeeds, and returns the supplied object.
String actualResult = assertTimeout(ofMinutes(2), () -> {
return "a result";
});
assertEquals("a result", actualResult);
}
@Test
void timeoutNotExceededWithMethod() {
// The following assertion invokes a method reference and returns an object.
String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
assertEquals("hello world!", actualGreeting);
}
@Test
void timeoutExceeded() {
// The following assertion fails with an error message similar to:
// execution exceeded timeout of 10 ms by 91 ms
assertTimeout(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
Thread.sleep(100);
});
}
@Test
void timeoutExceededWithPreemptiveTermination() {
// The following assertion fails with an error message similar to:
// execution timed out after 10 ms
assertTimeoutPreemptively(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
Thread.sleep(100);
});
}
private static String greeting() {
return "hello world!";
}
}
参数化测试
在 JUnit4 中,如果想要实现参数化测试(使用不同的参数来测试相同的一个方法),只能使用测试类中的字段来实现。而在 JUnit5 中,提供了参数化测试来实现这个需求。不同的参数值可以直接和一个测试方法关联,并且允许直接在一个测试类中提供不同的参数值直接参与测试,这些在 JUnit4 中都是无法实现的。
JUnit5 的参数化可以通过一组 CSV 格式的字符串、外部的 CSV、YML、JSON 文件、枚举、工厂方法,或者指定的提供类来提供。CSV 中的字符串类型的值还可以自动地转化为指定的类型,并且可以完成自己的类型转换器,如将 String 转成你希望的任何指定类型。
- @ValueSource:指定入参来源,支持八大基础类、String、Class 类型
- @NullSource:提供一个 null 入参
- @EnumSource:提供一个枚举入参
- @MethodSource:通过一个方法入参(该方法实现了自定义的数据获取方式)
- @CsvSource:提供 CSV 格式的入参
- @CsvFileSource:通过 CSV 文件提供参数
- @ArgumentsSource
简单参数与 CSV 参数
测试类:
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ParameterTest {
@DisplayName("Parameter Count : 1")
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void test1(int num1) {
assertTrue(num1 < 4);
}
@DisplayName("Parameter Count : 2")
@ParameterizedTest(name = "{index} ==> fruit=''{0}'', qty={1}")
@CsvSource({
"apple, 1",
"banana, 2"
})
void test2(String fruit, int qty) {
assertTrue(true);
}
@DisplayName("Parameter Count : 3")
@ParameterizedTest(name = "{index} ==> fruit=''{0}'', qty={1}, price={2}")
@CsvSource({
"apple, 1, 1.99",
"banana, 2, 2.99"
})
void test3(String fruit, int qty, BigDecimal price) {
assertTrue(true);
}
/**
* csv文件内容:
* name, age
* shawn, 24
*/
@DisplayName("参数化测试-从csv文件获取")
@ParameterizedTest
@CsvFileSource(resources="/test.csv", numLinesToSkip=1) // 指定csv文件位置,并忽略标题行
public void parameterizedTestWithCsv(String name, Integer age) {
System.out.println("name:" + name + ", age:" + age);
Assertions.assertNotNull(name);
Assertions.assertNotNull(age);
}
}
执行结果:
Json 数据驱动
测试类:
import com.bean.User;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.IOException;
import java.util.List;
public class ParameterTest {
/**
* Json文件内容:
* [
* {"name": "apple", "age": "12"},
* {"name": "banana", "age": "13"}
* ]
*/
static List<User> testDDTFromJson() throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
TypeReference typeReference = new TypeReference<List<User>>(){};
List<User> users = (List<User>) objectMapper.readValue(
ParameterTest.class.getResourceAsStream("/user.json"), // 本类名反射
typeReference
);
return users;
}
@ParameterizedTest
// @MethodSource("testDDTFromJson") // 指定获取数据源的方法名
@MethodSource // 若不指定方法名,则自动找同名方法
@DisplayName("从方法获取测试数据")
void testDDTFromJson(User user) {
System.out.println(user);
Assertions.assertTrue(user.name.length() > 3);
}
}
Yaml 数据驱动
Yaml 相关依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.3.3</version>
</dependency>
测试类:
import com.bean.User;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.IOException;
import java.util.List;
public class ParameterTest {
/**
* Yaml文件内容:
* - name: apple
* age: 12
* - name: banana
* age: 13
*/
static List<User> testDDTFromYaml() throws IOException {
ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
TypeReference typeReference = new TypeReference<List<User>>(){};
List<User> users = (List<User>) objectMapper.readValue(
ParameterTest.class.getResourceAsStream("/user.yaml"), // 本类名反射
typeReference
);
return users;
}
@ParameterizedTest
// @MethodSource("testDDTFromYaml") // 指定获取数据源的方法名
@MethodSource // 若不指定方法名,则自动找同名方法
@DisplayName("从方法获取测试数据")
void testDDTFromYaml(User user) {
System.out.println(user);
Assertions.assertTrue(user.name.length() > 3);
}
}
嵌套测试
JUnit5 提供了嵌套测试用于更好表示各个单元测试类之间的关系。平时我们写单元测试时一般都是一个类对应一个单元测试类,不过有些互相之间有业务关系的类,他们的单元测试完全是可以写在一起。因此,使用内嵌的方式表示,能够减少测试类的数量,防止类爆炸。
JUnit5 提供了 @Nested 注解,能够以静态成员内部类
的形式对测试用例类进行逻辑分组。
示例:
import org.junit.jupiter.api.*;
class NestedTest {
@BeforeEach
void init() {
System.out.println("init");
}
@Test
@DisplayName("Nested")
void test() {
System.out.println("test");
}
@Nested
@DisplayName("Nested2")
class Nested2 {
@BeforeEach
void Nested2_init() {
System.out.println("Nested2_init");
}
@Test
void Nested2_test1() {
System.out.println("Nested2_test1");
}
@Test
void Nested2_test2() {
System.out.println("Nested2_test2");
}
@Nested
@DisplayName("Nested3")
class Nested3 {
@BeforeEach
void Nested3_init() {
System.out.println("Nested3_init");
}
@Test
void Nested3_test1() {
System.out.println("Nested3_test1");
}
@Test
void Nested3_test2() {
System.out.println("Nested3_test2");
}
}
}
}
执行结果:
init
test
init
Nested2_init
Nested2_test1
init
Nested2_init
Nested2_test2
init
Nested2_init
Nested3_init
Nested3_test1
init
Nested2_init
Nested3_init
Nested3_test2
忽略测试
JUnit5 提供 @Disabled 禁用整个测试类或单个测试方法上的测试执行。
@Disabled("忽略执行的描述")
@Test
void Demo() {
System.out.println("忽略执行");
}
重复测试
重复运行单元测试可以更加保证测试的准确性,规避一些随机性带来的测试问题。
@RepeatedTest(10) // 表示重复执行10次
@DisplayName("重复测试")
public void testRepeated() {
Assertions.assertTrue(1==1);
}
前置条件
JUnit5 中的前置条件(Assumptions)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。
前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。在如下案例中:
- assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试方法执行终止。
- assumingThat 的参数是分别表示条件的布尔值和 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试方法的执行并不会终止。
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Objects;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;
public class AssumeTest {
private final String env = "dev1";
@Test
@DisplayName("assume simple")
public void testSimpleAssume() {
assumeTrue(Objects.equals(this.env, "dev1"));
System.out.println("环境是 dev1"); // 执行输出
assumeTrue(Objects.equals(this.env, "dev2")); // org.opentest4j.TestAbortedException: Assumption failed: assumption is not true
System.out.println("环境是 dev2"); // 不执行输出
}
@Test
@DisplayName("assume then do")
public void testAssumeThenDo() {
assumingThat( // 只有条件满足时,Executable 对象才会被执行
Objects.equals(this.env, "dev2"), // 表示条件的布尔值
() -> System.out.println("In dev1") // Executable 接口的实现对象
);
// 即使上述不满足条件,也会继续执行剩下代码
System.out.println("始终执行");
}
}
测试执行顺序
以下展示如何通过 MethodOrderer 类控制 JUnit5 的测试执行顺序。
Alphanumeric:字母数字顺序
示例:
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static org.junit.jupiter.api.Assertions.assertEquals;
@TestMethodOrder(MethodOrderer.Alphanumeric.class)
public class AlphanumericTest {
@Test
void testZ() {
assertEquals(2, 1 + 1);
}
@Test
void testA() {
assertEquals(2, 1 + 1);
}
@Test
void testY() {
assertEquals(2, 1 + 1);
}
@Test
void testE() {
assertEquals(2, 1 + 1);
}
@Test
void testB() {
assertEquals(2, 1 + 1);
}
}
执行结果:
testA()
testB()
testE()
testY()
testZ()
OrderAnnotation:根据 @Order 值排序
示例:
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static org.junit.jupiter.api.Assertions.assertEquals;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OrderAnnotationrTest {
@Test
void test0() {
assertEquals(2, 1 + 1);
}
@Test
@Order(3)
void test1() {
assertEquals(2, 1 + 1);
}
@Test
@Order(1)
void test2() {
assertEquals(2, 1 + 1);
}
@Test
@Order(2)
void test3() {
assertEquals(2, 1 + 1);
}
@Test
void test4() {
assertEquals(2, 1 + 1);
}
}
执行结果:
test2()
test3()
test1()
test0()
test4()
Random:随机顺序
示例:
@TestMethodOrder(MethodOrderer.Random.class)
class RandomTest {
@Test
void aTest() {}
@Test
void bTest() {}
@Test
void cTest() {}
}
还可配置自定义种子 junit.jupiter.execution.order.random.seed 以创建可重复的测试版本:
- 方式一:在 junit-platform.properties 属性文件中配置
junit.jupiter.execution.order.random.seed=99
- 方式二:在 Maven 的 pom.xml 中配置参数
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M3</version>
<configuration>
<properties>
<configurationParameters>
junit.jupiter.execution.order.random.seed=99
</configurationParameters>
</properties>
</configuration>
</plugin>
实现自定义顺序
示例:
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.math.BigDecimal;
import java.util.Comparator;
import static org.junit.jupiter.api.Assertions.assertTrue;
// 根据方法入参个数的顺序执行
class ParameterCountOrder implements MethodOrderer {
private Comparator<MethodDescriptor> comparator =
Comparator.comparingInt(md1 -> md1.getMethod().getParameterCount());
@Override
public void orderMethods(MethodOrdererContext context) {
context.getMethodDescriptors().sort(comparator.reversed());
}
}
@TestMethodOrder(ParameterCountOrder.class)
public class MethodParameterCountTest {
@DisplayName("Parameter Count : 2")
@ParameterizedTest(name = "{index} ==> fruit=''{0}'', qty={1}")
@CsvSource({
"apple, 1",
"banana, 2"
})
void test2(String fruit, int qty) {
assertTrue(true);
}
@DisplayName("Parameter Count : 1")
@ParameterizedTest(name = "{index} ==> ints={0}")
@ValueSource(ints = {1, 2, 3})
void test1(int num1) {
assertTrue(num1 < 4);
}
@DisplayName("Parameter Count : 3")
@ParameterizedTest(name = "{index} ==> fruit=''{0}'', qty={1}, price={2}")
@CsvSource({
"apple, 1, 1.99",
"banana, 2, 2.99"
})
void test3(String fruit, int qty, BigDecimal price) {
assertTrue(true);
}
}
执行结果:
动态测试
问题场景:运维团队有⼀批做线上配置检查脚本,领导希望将他们的测试结果整合到我们的 Junit 测试报告⾥,他们有⼏千条测试⽤例,⽽且是 shell 写成,使⽤ Java 重写⼯作量巨⼤。
问题原因:传统⾃动化测试思路中,我们的测试逻辑是在以硬编码的形式组织到代码⾥的,当遇到⽤例迁移或结果整合时,会产⽣⼤量的逻辑重写。
解决思路:除了硬编码的脚本编写⽅式外,还要能动态地在脚本 Runtime 时⽣成⽤例。
实施⽅案:JUnit5 提供了动态测试⽅案,让测试⼈员可以在脚本 Runtime 时动态的批量⽣成⽤例。
官⽅给出的 DynamicTest ⽰例:
案例实现
- Maven 依赖:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.9.9</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
- 配置文件:shell_test_result.yaml
resultList:
- caseName : 'case_1_1'
result : true
- caseName : 'case_1_2'
result : false
- caseName : 'case_2_1'
result : true
- caseName : 'case_2_2'
result : false
- caseName: 'case_1_1'
result: true
- caseName: 'case_1_2'
result: false
- caseName: 'case_2_1'
result: true
- caseName: 'case_2_2'
result: false
- caseName: 'case_1_1'
result: true
- caseName: 'case_1_2'
result: false
- caseName: 'case_2_1'
result: true
- caseName: 'case_2_2'
result: false
- 实体类:ShellResult.java
import lombok.Data;
@Data // 实现了getter和setter功能
public class ShellResult {
private String caseName;
private boolean result;
}
- 实体类:ResultList.java
import lombok.Data;
import java.util.List;
@Data
public class ResultList {
private List<ShellResult> resultList;
}
- 测试类:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import entity.ResultList;
import entity.ShellResult;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
public class Demo {
// 批量读取测试文件数据,动态生成并执行测试用例
@TestFactory
Collection<DynamicTest> runShellResult() throws IOException {
List<DynamicTest> dynamicTestList = new ArrayList<>();
ObjectMapper objectMApper = new ObjectMapper(new YAMLFactory());
// 反序列化yaml数据到对象列表中
ResultList resultList = objectMApper.readValue(new File("src/main/resources/shell_test_result.yaml"), ResultList.class);
System.out.println("Done!");
// 动态遍历生成测试方法
for(ShellResult shellResult: resultList.getResultList()){
// 动态生成测试方法代码
DynamicTest dynamicTest=dynamicTest(
shellResult.getCaseName(),
() -> {assertTrue(shellResult.isResult());}
);
// 收集操作
dynamicTestList.add(dynamicTest);
}
return dynamicTestList;
}
}
- 执行结果:
并发测试
默认情况下,JUnit Jupiter 测试是在单个线程中按顺序运行的。自 5.3 版起,作为可选功能,可以并发执行测试。
并发测试前提条件:测试用例之间没有依赖关系,容错性好。
Junit5 的并发测试是在 junit-platform.properties 文件中进行配置的,只要在 src/main/resources/ 目录下新建该文件,那么 Junit5 在执行测试方法时就会自动读取配置文件中的配置。
示例
src/main/resources/junit-platform.properties 文件内容:
# 是否开启并发执行
junit.jupiter.execution.parallel.enabled = true
# 是否支持方法级别多线程,参数为:same_thread/concurrent
junit.jupiter.execution.parallel.mode.default = concurrent
# 是否支持类级别多线程,参数为:same_thread/concurrent
junit.jupiter.execution.parallel.mode.classes.default = concurrent
# 指定线程池大小
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=2
测试程序:
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.parallel.Execution;
import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT;
public class ConcurrentTest {
@RepeatedTest(10)
@Execution(CONCURRENT)
void testConcurrent() {
System.out.println("当前线程id:" + Thread.currentThread().getId());
}
}
配置文件详解
Junit5 官方 User guide 对每个配置项进行了简单的解释:
junit.jupiter.execution.parallel.enabled=true
要启用并发执行,请将以上配置参数设置为 true。
# 是否支持方法级别多线程same_thread/concurrent
junit.jupiter.execution.parallel.mode.default=same_thread
# 是否支持类级别多线程same_thread/concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent
以上两个配置项是用来控制测试脚本是否在方法维度和类维度进行并发执行。它们具有相同的两个配置选项,官方 User guide 解释如下:
SAME_THREAD
:强制与前置方法使用的同一线程执行。例如,当在测试方法上使用时,该测试方法将在 @BeforeAll 或 @AfterAll 方法的线程中执行。CONCURRENT
:并发执行,除非资源锁强制在同一线程中执行。
说白了就是 SAME_THREAD 意味着单线程,而 CONCURRENT 意味着多线程。
官方 User guide 中有一个图来解读这两个配置项组合能达到什么样的效果:
线程池配置的三种方式
剩下这两个配置是用来控制线程池属性的:
# the maximum pool size can be configured using a ParallelExecutionConfigurationStrategy
junit.jupiter.execution.parallel.config.strategy = fix
junit.jupiter.execution.parallel.config.fixed.parallelism = 2
-
dynamic
:根据可用逻辑处理器数量乘以 junit.jupiter.execution.parallel.config.dynamic.factor 的配置参数(默认为 1)计算并行的线程池数量。也就是说配置一个固定的倍数后,框架会根据运行机器的算力自动配置线程。 -
fixed
:使用 junit.jupiter.execution.parallel.config.fixed.parallelism 的配置参数作为并行的线程池数量。 -
custom
:允许通过实现接口 ParallelExecutionConfigurationStrategy 来配置并行的线程池数量,junit.jupiter.execution.parallel.config.custom.class 属性用来配置实现接口的实现类。
测试套件
使用 JUnit5 测试套件,可以将测试扩展到多个测试类和不同的软件包。
JUnit5 提供了两个注解:@SelectPackages 和 @SelectClasses 来创建测试套件。而要执行该套件,需要配合使用 @RunWith(JUnitPlatform.class)。
注解 | 作用 |
---|---|
@RunWith(JUnitPlatform.class) | 测试套件(从 JUnit4 迁移过来的) |
@SelectPackage | 创建测试套件 |
@SelectClasses | 创建测试套件 |
@IncludePackage | 过滤需要执行的测试包 |
@ExcludePackages | 过滤不需要执行的测试包 |
@IncludeClassNamePatterns | 过滤需要执行的测试类 |
@ExcludeClassNamePatterns | 过滤不需要执行的测试类 |
@IncludeTags | 过滤需要执行的测试方法 |
@ExcludeTags | 过滤不需要执行的测试方法 |
添加 Maven 依赖:
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-runner</artifactId>
<version>1.6.1</version>
<scope>test</scope>
</dependency>
示例:@RunWith、@SelectPackages
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SelectPackages({"com.demo1", "com.demo2"})
public class SuiteTest {
}
示例:@SelectPackages、@SelectClasses
@RunWith(JUnitPlatform.class)
@SelectPackages({"com.demo1"})
@SelectClasses({BTest.class, CTest.class})
public class SuiteTest {
// 执行 @SelectPackages 和 @SelectClasses 并集后的测试方法
}
示例:@IncludePackages、@ExcludePackages
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.ExcludePackages;
import org.junit.platform.suite.api.IncludePackages;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SelectPackages({"com.demo1", "com.demo2"})
@IncludePackages({"com.demo1", "com.demo2"})
@ExcludePackages("com.demo2")
public class SuiteTest {
}
示例:@IncludeClassNamePatterns、@ExcludeClassNamePatterns
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.*;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SelectPackages({"com.demo1", "com.demo2"})
@IncludeClassNamePatterns("com.*Test")
@ExcludeClassNamePatterns({
"com.innder.*",
"com.*.CTest"
})
public class SuiteTest {
}
示例:@IncludeTags、@Tag
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.IncludeTags;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SelectPackages({"com.demo1", "com.demo2"})
@IncludeTags("TagDemo")
public class SuiteTest {
}
maven-surefire-plugin
什么是 maven-surefire-plugin ?
如果你执行过 mvn test 或者执行其他 maven 命令时跑了测试用例,你就已经用过 maven-surefire-plugin 了。maven-surefire-plugin 是 maven 里执行测试用例的插件,不显示配置就会用默认配置。这个插件的 surefire:test 命令会默认绑定 maven 执行的 test 阶段。
maven的生命周期有哪些阶段?
[validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy]
maven-surefire-plugin 的使用
如果说 maven 已经有了 maven-surefire-plugin 的默认配置,我们还有必要了解 maven-surefire-plugin 的配置么?答案是肯定的。虽说 maven-surefire-plugin 有默认配置,但是当需要修改一些测试执行的策略时,就有必要我们去重新配置这个插件了。
插件自动匹配:
最简单的配置方式就不配置或者是只声明插件。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19</version>
</plugin>
这个时候 maven-surefire-plugin 会按照如下逻辑去寻找 JUnit 的版本并执行测试用例。
if the JUnit version in the project >= 4.7 and the parallel attribute has ANY value
use junit47 provider
if JUnit >= 4.0 is present
use junit4 provider
else
use junit3.8.1
插件手动匹配:
示例:
<build>
<plugins>
<!-- 该插件能够在运行后自动在target目录生成allure测试结果目录 -->
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<includes>
<!-- 默认测试文件的命名规则:
"**/Test*.java"
"**/*Test.java"
"**/*Tests.java"
"**/*TestCase.java"
如果现有测试文件不符合以上命名,可以在 pom.xml 添加自定义规则
-->
<include>**/**.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
更多用法