JUnit5
JUnit是Java生态系统中最流行的单元测试框架之一。JUnit5版本包含许多令人兴奋的创新,其目标是支持Java8和更高版本中的新功能,并支持多种不同风格的测试。
Maven依赖
启动JUnit5.x.0非常简单;我们只需要将以下依赖项添加到pom.xml中:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
此外,集成开发工具,如Eclipse、IntelliJ 支持直接运行JUnit单元测试。新版本IntelliJ默认支持JUnit5。当然,开发者也可以使用Maven Test目标运行测试。
JUnit5 架构
JUnit5由三个不同子项目的不同模块组成, JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform
- JUnit Platform 负责在JVM上启动测试框架。它在JUnit及其客户端(如构建工具)之间定义了一个稳定而强大的接口。
- JUnit Platform 轻松地将客户端与JUnit集成,以发现和执行测试。
- 定义了TestEngine API,用于开发在JUnit平台上运行的测试框架。通过实现自定义测试引擎,我们可以将第三方测试库直接插入JUnit。
JUnit Jupiter
本模块包括用于在JUnit 5中编写测试的新编程和扩展模型。与JUnit 4相比,新注释如下:
- @TestFactory – 表示作为动态测试的测试工厂的方法
- @DisplayName – 定义测试类或测试方法的自定义显示名称
- @Nested – 表示带注释的类是嵌套的非静态测试类
- @Tag – 过滤筛选测试标记
- @ExtendWith – 注册自定义扩展
- @BeforeEach – 表示将在每个测试方法之前执行带注释的方法(相当于之前的@before)
- @AfterEach –表示将在每个测试方法之后执行带注释的方法(相当于之前的@after)
- @BeforeAll – 表示注释的方法将在当前类中的所有测试方法之前执行(相当于之前的@BeforeClass)
- @AfterAll – 表示注释方法将在当前类中的所有测试方法之后执行(相当于之前的@AfterClass)
- @Disable – 禁用测试类或方法(相当于之前的@Ignore)
JUnit Vintage
JUnit Vintage支持在JUnit5平台上运行基于JUnit3和JUnit4的测试。
基础注解
@BeforeAll @BeforeEach
package com.andy.spring.boot.docker.junit5;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class JunitTest {
@BeforeAll
public static void setUp(){
System.out.println("只运行一次,在所有的测试方法执行前运行");
}
@BeforeEach
public void init() {
System.out.println("@BeforeEach - 在每个测试方法之前都会运行");
}
@Test
public void test1() {
System.out.println("test - 执行Test 1 测试方法");
}
@Test
public void test2() {
System.out.println("test - 执行Test 2 测试方法");
}
}
如下图,执行在测试类上执行 Run JunitTest. 注意:@BeforeAll 注解方法必须为静态方法,使用static关键字修饰
@DisplayName @Disabled
package com.andy.spring.boot.docker.junit5;
import org.junit.jupiter.api.*;
public class JunitTest2 {
@DisplayName("Single test successful")
@Test
void testSingleSuccessTest() {
System.out.println("Success");
}
@Test
@Disabled("Not implemented yet")
void testShowSomething() {
System.out.println("testShowSomething");
}
}
如下图,执行在测试类上执行 Run JunitTest2, 使用 @Disabled 注解的测试方法被禁用并没有执行
@AfterEach @AfterAll
package com.andy.spring.boot.docker.junit5;
import org.junit.jupiter.api.*;
public class JunitTest3 {
@AfterAll
public static void setUp(){
System.out.println("只运行一次,在所有的测试方法执行后运行");
}
@AfterEach
public void init() {
System.out.println("@BeforeEach - 在每个测试方法之后都会运行");
}
@Test
public void test1() {
System.out.println("test - 执行Test 1 测试方法");
}
@Test
public void test2() {
System.out.println("test - 执行Test 2 测试方法");
}
}
这两个注解跟之前的@BeforeAll、@BeforeEach效果类似,只是执行顺序相反。注意:@BeforeAll 注解方法必须为静态方法,使用static关键字修饰
@ParameterizedTest
表示方法是参数化测试。除非重写这些方法,否则将继承这些方法。
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.util.StringUtils;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class JunitTest8 {
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(!StringUtils.isEmpty(candidate));
}
}
如下图,将预定义的参数逐个传入测试方法执行
@RepeatedTest
@RepeatedTest(10)
void repeatedTest() {
System.out.println("hello world");
}
如下执行结果,表示被测试方法可重复执行
@TestClassOrder
@Nested标注的嵌套试类中,使用@TestClassOrder指定不同类的执行顺序。这样的注释是继承的。
import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
class OrderedNestedTestClassesDemo {
@Nested
@Order(1)
class PrimaryTests {
@Test
void test1() {
System.out.println("test1");
}
}
@Nested
@Order(2)
class SecondaryTests {
@Test
void test2() {
System.out.println("test2");
}
}
}
@TestMethodOrder
真正的单元测试通常不应依赖于它们的执行顺序,但有时需要强制执行特定的测试方法执行顺序 — 例如,在编写集成测试或功能测试时,测试顺序很重要。
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(OrderAnnotation.class)
class OrderedTestsDemo {
@Test
@Order(1)
void nullValues() {
System.out.println(" test 1");
}
@Test
@Order(2)
void emptyValues() {
System.out.println(" test 2");
}
@Test
@Order(3)
void validValues() {
System.out.println(" test 3");
}
}
@TestInstance
在学习这个注解之前,我们先看以下的测试代码
class AdditionTest {
private int sum = 1;
@Test
void addingTwoReturnsThree() {
sum += 2;
assertEquals(3, sum);
}
@Test
void addingThreeReturnsFour() {
sum += 3;
assertEquals(4, sum);
}
}
在测试类上按照顺序依次执行这两个测试方法,均测试通过。这是由于在默认情况下,每个测试方法执行之前,sum都会被初始化一次,赋值为1。那么有没有可能需要保留之前测试方法计算之后的值呢?
@TestInstance就是干这个用的,它有两个设置级别:
- TestInstance.Lifecycle.PER_METHOD - 变量在测试方法之前重新初始化,默认值
- TestInstance.Lifecycle.PER_CLASS - 变量的作用范围在每个测试类,即会保留之前测试方法计算后的值
当在测试类上新增 @TestInstance(TestInstance.Lifecycle.PER_CLASS) ,测试结果就不正常了(如下图,理由是保留之前的计算值)
@Tag
测试类、测试方法都可以增加 @Tag 注解标记,这些标记稍后可用于过滤测试发现和执行。
@Test
@Tag("IntegrationTest")
public void testAddEmployeeUsingSimpelJdbcInsert() {
}
@Test
@Tag("UnitTest")
public void givenNumberOfEmployeeWhenCountEmployeeThenCountMatch() {
}
假设定义以上两个测试方法,并打上不同的标记。后续可以使用标记过滤特定场景的测试要求,如下:
@SelectPackages("com.baeldung.tags")
@IncludeTags("UnitTest")
public class EmployeeDAOUnitTestSuite {
}
运行 com.baeldung.tags 包下标记为 UnitTest 的所有测试方法
@ExtendWith
开发者使用@ExtendWith注解扩展一个或多个扩展类。其目的是扩展测试类、测试方法的表现行为,方便模块化测试工作中的代码复用。
-
定义扩展类
import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; public class MyExtension implements BeforeEachCallback, AfterEachCallback { @Override public void beforeEach(ExtensionContext context) throws Exception { System.out.println("MyExtension.beforeEach()"); } @Override public void afterEach(ExtensionContext context) throws Exception { System.out.println("MyExtension.afterEach()"); } }
-
扩展测试类
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(MyExtension.class) public class JUnit5ExtensionTest { @Test void test1() { System.out.println(" test1()"); } @Test void test2() { System.out.println(" test2()"); } }
@RegisterExtension
开发者可以将此注释应用于测试类中的字段。这种方法的一个优点是,我们可以直接将扩展作为测试内容中的对象进行访问。JUnit将在适当的阶段调用扩展方法。例如,如果扩展实现BeforeEachCallback,JUnit将在执行测试方法之前调用其相应的接口方法。
-
定义扩展
import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; public class LoggingExtension implements BeforeAllCallback, BeforeEachCallback { // logger, constructor etc @Override public void beforeAll(ExtensionContext extensionContext) throws Exception { System.out.println("beforeAll : " + extensionContext.getDisplayName()); } @Override public void beforeEach(ExtensionContext extensionContext) throws Exception { System.out.println("beforeEach : " + extensionContext.getDisplayName()); } }
-
注册扩展
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; public class RegisterExtensionTest { @RegisterExtension static LoggingExtension staticExtension = new LoggingExtension(); @Test public void demoTest() { // assertions } }
-
验证测试
JUnit 断言
JUnit5在语法上使用Java8的一些新特性,尤其是lambda表达式。
Assertions
断言已移动到org.unit.jupiter.api。断言和已显著改进。如前所述,开发者可以在断言中使用lambdas:
public class JunitTest4 {
@Test
void lambdaExpressions() {
List<Integer> numbers = Arrays.asList(1, 2, 3);
assertTrue(numbers.stream().mapToInt(t -> t.intValue()).sum() > 5, () -> "Sum should be greater than 5");
}
}
尽管上面的示例很简单,但对断言消息使用lambda表达式的一个优点是它的求值很慢,如果消息构造很昂贵,这可以节省时间和资源。
开发者还可以使用assertAll()对断言进行分组,这将使用MultipleFailuresError报告组内的任何失败断言:
@Test
void groupAssertions() {
int[] numbers = {0, 1, 2, 3, 4};
assertAll("numbers",
() -> assertEquals(numbers[0], 1),
() -> assertEquals(numbers[3], 3),
() -> assertEquals(numbers[4], 1)
);
}
执行结果如下:
Assumptions
假设仅在满足某些条件时用于运行测试。这通常用于测试正常运行所需外部依赖条件,但与所测试的内容没有直接关系。开发者可以使用assumeTrue()、assumeFalse()和assumeThat()声明一个假设:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.*;
public class JunitTest5 {
@Test
void trueAssumption() {
assumeTrue(5 > 1);
assertEquals(5 + 2, 7);
}
@Test
void falseAssumption() {
assumeFalse(5 < 1);
assertEquals(5 + 2, 7);
}
@Test
void assumptionThat() {
String someString = "Just a string";
assumingThat(
someString.equals("Just a string"),
() -> assertEquals(2 + 2, 4)
);
}
}
如果假设失败,则抛出TestAbortedException,并跳过测试。
JUnit5 高级特性
异常测试
JUnit 5中有两种异常测试方法,我们可以使用assertThrows()方法实现这两种方法:
package com.andy.spring.boot.docker.junit5;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class JunitTest6 {
@Test
void shouldThrowException() {
Throwable exception = assertThrows(UnsupportedOperationException.class, () -> {
throw new UnsupportedOperationException("Not supported");
});
assertEquals("Not supported", exception.getMessage());
}
@Test
void assertThrowsException() {
String str = null;
assertThrows(IllegalArgumentException.class, () -> {
Integer.valueOf(str);
});
}
}
第一个示例验证抛出的异常的详细信息,第二个示例验证异常的类型。
测试组件
接下来学习一个JUnit5的新特性 Test Suites - 测试组件。将探讨在一个测试场景中聚合多个测试类的概念,以便我们可以一起运行这些测试类。JUnit5提供了两个注释@SelectPackages和@SelectClasses来创建测试组件。
@Suite
@SelectPackages("com.baeldung")
@ExcludePackages("com.baeldung.suites")
public class AllUnitTest {}
@SelectPackage用于指定运行测试组件时要选择的包的名称。在我们的示例中,它将运行所有测试。@ExcludePackages排除指定包下面的所有测试类。
@Suite
@SelectClasses({AssertionTest.class, AssumptionTest.class, ExceptionTest.class})
public class AllUnitClassTest {}
@SelectClasses注解用于指定运行测试套件时要选择的类。请注意,上述三个测试类可以不在同一个package下面。
Test Suites对于大规模的单元测试比较友好,开发/测试人员只需要编写测试用例,就可以大批量的运行测试代码,提高了开发的效率
动态测试
JUnit5的动态测试特性,它允许我们声明和运行运行时生成的测试用例。与静态测试(在编译时定义固定数量的测试用例)相反,动态测试允许我们在运行时动态定义测试用例。动态测试可以通过带有@TestFactory注释的工厂方法生成,请看一下代码:
package com.andy.spring.boot.docker.junit5;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class JunitTest7 {
private List<String> in = new ArrayList<>(Arrays.asList("Hello", "Yes", "No"));
private List<String> out = new ArrayList<>(Arrays.asList("Cześć", "Tak", "Nie"));
@TestFactory
Stream<DynamicTest> translateDynamicTestsFromStream() {
// 1. 循环list数组 每个值都进行动态测试 并赋予特定的显示名称
return in.stream()
.map(word -> DynamicTest.dynamicTest("Test translate " + word, () -> {
int id = in.indexOf(word);
assertEquals(out.get(id), translate(word));
}));
}
private String translate(String word) {
if ("Hello".equalsIgnoreCase(word)) {
return "Cześć";
} else if ("Yes".equalsIgnoreCase(word)) {
return "Tak";
} else if ("No".equalsIgnoreCase(word)) {
return "Nie";
}
return "Error";
}
}
这个例子非常简单,假设希望使用两个ArrayList来翻译单词,分别命名为in和out。工厂方法必须返回Stream、Collection、Iterable或Iterator。在我们的例子中,我们选择了一个Java8Stream。
请注意@TestFactory方法不能是私有的或静态的。测试的数量是动态的,它取决于ArrayList的大小。