1.概述
1.1. What is JUnit 5?
-
与以前的JUnit版本不同,JUnit 5由来自三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit平台+ JUnit Jupiter + JUnit Vintage
-
该JUnit的平台可作为一个基础发射测试框架在JVM上。它还定义了
TestEngine用于开发在平台上运行的测试框架的API。此外,该平台还提供了一个用于从命令行启动该平台的 控制台启动器和一个用于在基于JUnit 4的环境中的平台上运行任何平台的基于JUnit 4的运行器TestEngine -
JUnit Jupiter是新的编程模型和 扩展模型的组合,用于在JUnit 5中编写测试和扩展。Jupiter子项目提供了一个
TestEngine在平台上运行基于Jupiter的测试的功能。 -
JUnit Vintage提供了一个
TestEngine在平台上运行基于JUnit 3和JUnit 4的测试的功能。
1.4.2.JUnit 5功能
2.编写测试用例
2.1 第一个测试用例
-
下面的示例简要介绍了在JUnit Jupiter中编写测
import static org.junit.jupiter.api.Assertions.assertEquals; import example.util.Calculator; import org.junit.jupiter.api.Test; class MyFirstJUnitJupiterTests { private final Calculator calculator = new Calculator(); @Test void addition() { assertEquals(2, calculator.add(1, 1)); } }
2.2 注解
JUnit Jupiter支持以下用于配置测试和扩展框架的注释。
| 注解 | 描述 |
|---|---|
@Test | 表示方法是测试方法。与JUnit 4的@Test注释不同,此注释不声明任何属性,因为JUnit Jupiter中的测试扩展基于其自己的专用注释进行操作。除非重写这些方法,否则它们将被**继承。 |
@ParameterizedTest | 表示方法是参数化测试。除非重写这些方法,否则它们将被**继承。 |
@RepeatedTest | 表示方法是重复测试的测试模板。除非重写这些方法,否则它们将被**继承。 |
@TestFactory | 表示方法是用于动态测试的测试工厂。除非重写这些方法,否则它们将被**继承。 |
@TestTemplate | 表示方法是测试用例的模板,测试用例设计为根据已注册提供程序返回的调用上下文的数量来多次调用。除非重写这些方法,否则它们将被**继承。 |
@TestMethodOrder | 用于为带注释的测试类配置测试方法的执行顺序;类似于JUnit 4的@FixMethodOrder。这样的注释是继承的。 |
@TestInstance | 用于为带注释的测试类配置测试实例生命周期。这样的注释是继承的。 |
@DisplayName | 声明测试类或测试方法的自定义显示名称。这样的注释不是继承的。 |
@DisplayNameGeneration | 声明测试类的自定义显示名称生成器。这样的注释是继承的。 |
@BeforeEach | 表示该注释的方法应该被执行之前 的每个 @Test,@RepeatedTest,@ParameterizedTest,或@TestFactory方法在当前类; 类似于JUnit 4的@Before。除非重写这些方法,否则它们将被**继承。 |
@AfterEach | 表示该注释的方法应该被执行之后 每个 @Test,@RepeatedTest,@ParameterizedTest,或@TestFactory方法在当前类; 类似于JUnit 4的@After。除非重写这些方法,否则它们将被**继承。 |
@BeforeAll | 表示该注释的方法应该被执行之前 所有 @Test,@RepeatedTest,@ParameterizedTest,和@TestFactory方法在当前类; 类似于JUnit 4的@BeforeClass。此类方法是继承的(除非它们被隐藏或覆盖),并且必须被继承(除非static使用“每类”测试实例生命周期)。 |
@AfterAll | 表示该注释的方法应该被执行之后 的所有 @Test,@RepeatedTest,@ParameterizedTest,和@TestFactory方法在当前类; 类似于JUnit 4的@AfterClass。此类方法是继承的(除非它们被隐藏或覆盖),并且必须被继承(除非static使用“每类”测试实例生命周期)。 |
@Nested | 表示带注释的类是一个非静态的嵌套测试类。@BeforeAll和@AfterAll方法不能直接在使用@Nested测试类除非“每级”测试实例的生命周期被使用。这样的注释不是继承的。 |
@Tag | 用于在类或方法级别上声明用于过滤测试的标签;类似于TestNG中的测试组或JUnit 4中的类别。此类注释在类级别继承,而不在方法级别继承。 |
@Disabled | 用于禁用测试类或测试方法;类似于JUnit 4的@Ignore。这样的注释不是继承的。 |
@Timeout | 如果执行超过给定的持续时间,则使测试,测试工厂,测试模板或生命周期方法失败。这样的注释是继承的。 |
@ExtendWith | 用于声明性地注册扩展。这样的注释是继承的。 |
@RegisterExtension | 用于通过字段以编程方式注册扩展。除非被遮盖,否则这些字段将被继承。 |
@TempDir | 用于通过生命周期方法或测试方法中的字段注入或参数注入来提供临时目录;位于org.junit.jupiter.api.io包装中。 |
2.2.1 元注释和组合注释
JUnit Jupiter批注可以用作元批注。这意味着您可以定义自己的组合注释,该注释将自动继承其元注释的语义。
例如,而不是复制和粘贴@Tag("fast")整个代码库(见 标签和过滤),您可以创建自定义组成的注释 命名@Fast如下。@Fast然后可以用作的替代产品 @Tag("fast")。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Tag;
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface Fast {
}
2.2.2.1.1 过滤标签
- 标签不能为
null或为空。 - 一个修剪标签不能包含空格。
- 一个修剪标签不得包含ISO控制字符。
- 一个修剪标签不得包含以下任何保留字符。
,:逗号(:左括号):右括号&:连字号|:竖线!:感叹号
2.2.2.测试类别和方法
测试类:任何顶层类,static构件类,或@Nested类包含至少一个测试方法。
测试类不能是abstract并且必须具有单个构造函数。
测试方法:即直接注释或元注解的任何实例方法 @Test,@RepeatedTest,@ParameterizedTest,@TestFactory,或@TestTemplate。
生命周期方法:即直接注释或与元注释的任何方法 @BeforeAll,@AfterAll,@BeforeEach,或@AfterEach。
测试方法和生命周期方法可以在当前测试类中本地声明,从超类继承或从接口继承(请参见“ 测试接口和默认方法”)。此外,测试方法和生命周期方法不得为abstract且不得返回值。
测试类,测试方法,以及生命周期方法是不是必需的public,但他们一定不会是private。 | |
|---|---|
以下测试类演示@Test方法的使用以及所有受支持的生命周期方法。有关运行时语义的更多信息,请参见 测试执行顺序和 回调的包装行为。
标准考测试如下
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class StandardTests {
@BeforeAll
static void initAll() {
}
@BeforeEach
void init() {
}
@Test
void succeedingTest() {
}
@Test
void failingTest() {
fail("a failing test");
}
@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
}
@Test
void abortedTest() {
assumeTrue("abc".contains("Z"));
fail("test should have been aborted");
}
@AfterEach
void tearDown() {
}
@AfterAll
static void tearDownAll() {
}
}
2.2.4.显示名称
-
测试类和测试方法可以通过以下方式声明自定义显示名称
@DisplayName:带有空格,特殊字符,甚至是表情符号,这些名称将显示在测试报告中,并由测试运行器和IDE显示。import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @DisplayName("A special test case") class DisplayNameDemo { @Test @DisplayName("Custom test name containing spaces") void testWithDisplayNameContainingSpaces() { } @Test @DisplayName("╯°□°)╯") void testWithDisplayNameContainingSpecialCharacters() { } @Test @DisplayName("😱") void testWithDisplayNameContainingEmoji() { } }
2.3.断言
-
JUnit Jupiter附带了JUnit 4拥有的许多断言方法,并添加了一些非常适合与Java 8 lambda一起使用的方法。所有JUnit Jupiter断言都是该类中的
static方法org.junit.jupiter.api.Assertions。import static java.time.Duration.ofMillis; import static java.time.Duration.ofMinutes; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTimeout; import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.concurrent.CountDownLatch; import example.domain.Person; import example.util.Calculator; import org.junit.jupiter.api.Test; class AssertionsDemo { private final Calculator calculator = new Calculator(); private final Person person = new Person("Jane", "Doe"); @Test void standardAssertions() { assertEquals(2, calculator.add(1, 1)); assertEquals(4, calculator.multiply(2, 2), "The optional failure message is now the last parameter"); assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- " + "to avoid constructing complex messages unnecessarily."); } @Test void groupedAssertions() { // In a grouped assertion all assertions are executed, and all // failures will be reported together. assertAll("person", () -> assertEquals("Jane", 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("e")) ); }, () -> { // 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() { Exception exception = assertThrows(ArithmeticException.class, () -> calculator.divide(1, 0)); assertEquals("/ by zero", 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. new CountDownLatch(1).await(); }); } private static String greeting() { return "Hello, World!"; } }
2.4. 测试执行顺序
默认情况下,将使用确定性但故意不明显的算法对测试方法进行排序。这样可以确保测试套件的后续运行以相同的顺序执行测试方法,从而允许可重复的构建。
-
尽管真正的单元测试通常不应该依赖于它们执行的顺序,但有时还是有必要强制执行特定的测试方法执行顺序,例如,在编写集成测试或功能测试时,测试顺序是重要,尤其是与结合使用
@TestInstance(Lifecycle.PER_CLASS)。-
要控制执行测试方法的顺序,请用注释您的测试类或测试接口,
@TestMethodOrder并指定所需的MethodOrderer实现。您可以实现自己的自定义MethodOrderer或使用以下内置MethodOrderer实现之一。
DisplayName:根据测试方法的显示名称按字母顺序对它们进行排序(请参阅 显示名称生成优先级规则)MethodName:根据测试方法的名称和形式参数列表按字母数字顺序对其进行排序。OrderAnnotation:根据通过注释指定的值对 测试方法进行数字排序@Order。
-
下面的示例演示如何保证测试方法以通过@Order注释指定的顺序执行。
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() {
// perform assertions against null values
}
@Test
@Order(2)
void emptyValues() {
// perform assertions against empty values
}
@Test
@Order(3)
void validValues() {
// perform assertions against valid values
}
}
2.5.嵌套测试
@Nested测试为测试编写者提供了更多功能来表达几组测试之间的关系。这是一个详尽的示例。
嵌套测试套件,用于测试堆栈
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.EmptyStackException;
import java.util.Stack;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
2.6.参数化测试
通过参数化测试,可以使用不同的参数多次运行测试。它们的声明与常规@Test方法一样,但是使用 @ParameterizedTest批注。此外,您必须声明至少一个 源,该源将为每次调用提供参数,然后在test方法中使用这些参数。
下面的示例演示了一个参数化测试,该测试使用@ValueSource 批注将String数组指定为参数源。
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
2.7. 重复测试
JUnit Jupiter通过注释方法@RepeatedTest并指定所需的总重复次数,可以重复指定次数的测试。每次重复测试的行为都类似于常规@Test方法的执行, 并且完全支持相同的生命周期回调和扩展。
下面的示例演示如何声明一个名为test的测试repeatedTest(),该测试将自动重复10次。
除了指定重复次数之外,还可以通过 注释的name属性为每次重复配置自定义显示名称@RepeatedTest。此外,显示名称可以是由静态文本和动态占位符组合而成的模式。当前支持以下占位符。
DisplayName:显示@RepeatedTest方法名称{currentRepetition}:当前的重复计数{totalRepetitions}:重复总数
给定重复的默认显示名称是根据以下模式生成的:"repetition {currentRepetition} of {totalRepetitions}"。因此,上一个repeatedTest()示例的单个重复的显示名称为: repetition 1 of 10,repetition 2 of 10等。如果您希望@RepeatedTest每个重复名称中包含方法的显示名称,则可以定义自己的自定义模式或使用预定义的RepeatedTest.LONG_DISPLAY_NAME模式。后者等于其结果在显示名称为个别重复喜欢 ,等"DisplayName :: repetition {currentRepetition} of {totalRepetitions}"``repeatedTest() :: repetition 1 of 10``repeatedTest() :: repetition 2 of 10
为了检索有关当前重复和重复编程的总数的信息,一个开发者可以选择具有的实例 RepetitionInfo注入@RepeatedTest,@BeforeEach或@AfterEach方法。
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.logging.Logger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.TestInfo;
class RepeatedTestsDemo {
private Logger logger = // ...
@BeforeEach
void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
int currentRepetition = repetitionInfo.getCurrentRepetition();
int totalRepetitions = repetitionInfo.getTotalRepetitions();
String methodName = testInfo.getTestMethod().get().getName();
logger.info(String.format("About to execute repetition %d of %d for %s", //
currentRepetition, totalRepetitions, methodName));
}
@RepeatedTest(10)
void repeatedTest() {
// ...
}
@RepeatedTest(5)
void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) {
assertEquals(5, repetitionInfo.getTotalRepetitions());
}
@RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
@DisplayName("Repeat!")
void customDisplayName(TestInfo testInfo) {
assertEquals("Repeat! 1/1", testInfo.getDisplayName());
}
@RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
@DisplayName("Details...")
void customDisplayNameWithLongPattern(TestInfo testInfo) {
assertEquals("Details... :: repetition 1 of 1", testInfo.getDisplayName());
}
@RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}")
void repeatedTestInGerman() {
// ...
}
}
3.运行测试
推荐使用IDEA 2017.3或更新版本,因为IDEA的这些新版本将自动下载基于在项目中使用的API版本以下JAR: ,junit-platform-launcher, junit-jupiter-engine和junit-vintage-engine。
Maven依赖项
<!-- Only needed to run tests in a version of IntelliJ IDEA that bundles older versions -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
4.依赖图
5.单元测试 AIR 原则
好的单元测试必须遵守 AIR 原则,即 Automatic(自动化)、Independent(独立性)、Repeatable(可重复)。
Automatic(自动化)
单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元 测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。
Independent(独立性)
保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。
Repeatable(可重复)
单元测试是可以重复执行的,不能受到外界环境的影响。因为单元测试通常会被放到持续集成中,每次有代码 check in时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。
为了不受外界环境影响,要求设计代码时就把 SUT 的依赖改成注入,在测试时用 spring这样的 DI 框架注入一个本地(内存)实现或者 Mock 实现。
单元测试粒度
对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别。只有测试粒度小才能在出错时尽快定位到出错位置。单测不负责检查跨类或者跨系统的交互逻辑,那是集成测试的领域。
单元测试范围
核心业务、核心应用、核心模块的增量代码确保单元测试通过。新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正。
其它规范
1、单元测试代码必须写在如下工程目录 src/test/java,不允许写在业务代码目录下。
2、单元测试的基本目标:语句覆盖率达到 70%;核心模块的语句覆盖率和分支覆盖率都要达到 100%。
3、在工程规约的应用分层中提到的 DAO 层,Manager 层,可重用度高的 Service,都应该进行单元测试。
4、编写单元测试代码遵守 BCDE 原则,以保证被测试模块的交付质量。
阿里巴巴 Java 开发手册 \9. 【推荐】编写单元测试代码遵守 BCDE 原则,以保证被测试模块的交付质量。
- Border:边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等;
- Correct:正确的输入,并得到预期的结果;
- Design:与设计文档相结合,来编写单元测试;
- Error:强制错误信息输入(如:非法数据、异常流程、非业务允许输入等),并得到预期的结果;
5、对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的, 或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据。
6、和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者 对单元测试产生的数据有明确的前后缀标识。
7、对于不可测的代码建议做必要的重构,使代码变得可测,避免为了达到测试要求而书写不规范测试代码。
8、在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好覆盖所有测试用例。
9、单元测试作为一种质量保障手段,不建议项目发布后补充单元测试用例,建议在项目提测前完成单元测试。
10、为了更方便地进行单元测试,业务代码应避免以下情况:
- 构造方法中做的事情过多
- 存在过多的全局变量和静态方法
- 存在过多的外部依赖
- 存在过多的条件语句。说明:多层条件语句建议使用卫语句、策略模式、状态模式等方式重构
11、不要对单元测试存在如下误解:
- 那是测试同学干的事情
- 单元测试代码是多余的。系统的整体功能与各单元部件的测试正常与否是强相关的
- 单元测试代码不需要维护,一年半载后,那么单元测试几乎处于废弃状态
- 单元测试与线上故障没有辩证关系,好的