JUnit是Java生态系统中最流行的单元测试框架之一。JUnit5版本包含许多令人兴奋的创新,其目标是支持Java8和更高版本中的新功能,并支持多种不同风格的测试。
启动JUnit5.x.0非常简单;我们只需要将以下依赖项添加到pom.xml中:
org.junit.jupiter junit-jupiter-engine 5.8.1 test
此外,集成开发工具,如Eclipse、IntelliJ 支持直接运行JUnit单元测试。新版本IntelliJ默认支持JUnit5。当然,开发者也可以使用Maven Test目标运行测试。
JUnit5由三个不同子项目的不同模块组成, JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
本模块包括用于在JUnit 5中编写测试的新编程和扩展模型。与JUnit 4相比,新注释如下:
JUnit Vintage支持在JUnit5平台上运行基于JUnit3和JUnit4的测试。
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 {@BeforeAllpublic static void setUp(){System.out.println("只运行一次,在所有的测试方法执行前运行");}@BeforeEachpublic void init() {System.out.println("@BeforeEach - 在每个测试方法之前都会运行");}@Testpublic void test1() {System.out.println("test - 执行Test 1 测试方法");}@Testpublic void test2() {System.out.println("test - 执行Test 2 测试方法");}
}
如下图,执行在测试类上执行 Run JunitTest. 注意:@BeforeAll 注解方法必须为静态方法,使用static关键字修饰
package com.andy.spring.boot.docker.junit5;import org.junit.jupiter.api.*;public class JunitTest2 {@DisplayName("Single test successful")@Testvoid testSingleSuccessTest() {System.out.println("Success");}@Test@Disabled("Not implemented yet")void testShowSomething() {System.out.println("testShowSomething");}
}
如下图,执行在测试类上执行 Run JunitTest2, 使用 @Disabled 注解的测试方法被禁用并没有执行
package com.andy.spring.boot.docker.junit5;import org.junit.jupiter.api.*;public class JunitTest3 {@AfterAllpublic static void setUp(){System.out.println("只运行一次,在所有的测试方法执行后运行");}@AfterEachpublic void init() {System.out.println("@BeforeEach - 在每个测试方法之后都会运行");}@Testpublic void test1() {System.out.println("test - 执行Test 1 测试方法");}@Testpublic void test2() {System.out.println("test - 执行Test 2 测试方法");}
}
这两个注解跟之前的@BeforeAll、@BeforeEach效果类似,只是执行顺序相反。注意:@BeforeAll 注解方法必须为静态方法,使用static关键字修饰
表示方法是参数化测试。除非重写这些方法,否则将继承这些方法。
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(10)void repeatedTest() {System.out.println("hello world");}
如下执行结果,表示被测试方法可重复执行
@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 {@Testvoid test1() {System.out.println("test1");}}@Nested@Order(2)class SecondaryTests {@Testvoid test2() {System.out.println("test2");}}
}
真正的单元测试通常不应依赖于它们的执行顺序,但有时需要强制执行特定的测试方法执行顺序 — 例如,在编写集成测试或功能测试时,测试顺序很重要。
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");}}
在学习这个注解之前,我们先看以下的测试代码
class AdditionTest {private int sum = 1;@Testvoid addingTwoReturnsThree() {sum += 2;assertEquals(3, sum);}@Testvoid addingThreeReturnsFour() {sum += 3;assertEquals(4, sum);}
}
在测试类上按照顺序依次执行这两个测试方法,均测试通过。这是由于在默认情况下,每个测试方法执行之前,sum都会被初始化一次,赋值为1。那么有没有可能需要保留之前测试方法计算之后的值呢?
@TestInstance就是干这个用的,它有两个设置级别:
当在测试类上新增 @TestInstance(TestInstance.Lifecycle.PER_CLASS) ,测试结果就不正常了(如下图,理由是保留之前的计算值)
测试类、测试方法都可以增加 @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注解扩展一个或多个扩展类。其目的是扩展测试类、测试方法的表现行为,方便模块化测试工作中的代码复用。
定义扩展类
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 {@Overridepublic void beforeEach(ExtensionContext context) throws Exception {System.out.println("MyExtension.beforeEach()");}@Overridepublic 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 {@Testvoid test1() {System.out.println(" test1()");}@Testvoid test2() {System.out.println(" test2()");}
}
开发者可以将此注释应用于测试类中的字段。这种方法的一个优点是,我们可以直接将扩展作为测试内容中的对象进行访问。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 implementsBeforeAllCallback, BeforeEachCallback {// logger, constructor etc@Overridepublic void beforeAll(ExtensionContext extensionContext)throws Exception {System.out.println("beforeAll : " + extensionContext.getDisplayName());}@Overridepublic 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 {@RegisterExtensionstatic LoggingExtension staticExtension = new LoggingExtension();@Testpublic void demoTest() {// assertions}
}
验证测试
JUnit5在语法上使用Java8的一些新特性,尤其是lambda表达式。
断言已移动到org.unit.jupiter.api。断言和已显著改进。如前所述,开发者可以在断言中使用lambdas:
public class JunitTest4 {@Testvoid lambdaExpressions() {List numbers = Arrays.asList(1, 2, 3);assertTrue(numbers.stream().mapToInt(t -> t.intValue()).sum() > 5, () -> "Sum should be greater than 5");}
}
尽管上面的示例很简单,但对断言消息使用lambda表达式的一个优点是它的求值很慢,如果消息构造很昂贵,这可以节省时间和资源。
开发者还可以使用assertAll()对断言进行分组,这将使用MultipleFailuresError报告组内的任何失败断言:
@Testvoid groupAssertions() {int[] numbers = {0, 1, 2, 3, 4};assertAll("numbers",() -> assertEquals(numbers[0], 1),() -> assertEquals(numbers[3], 3),() -> assertEquals(numbers[4], 1));}
执行结果如下:
假设仅在满足某些条件时用于运行测试。这通常用于测试正常运行所需外部依赖条件,但与所测试的内容没有直接关系。开发者可以使用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 {@Testvoid trueAssumption() {assumeTrue(5 > 1);assertEquals(5 + 2, 7);}@Testvoid falseAssumption() {assumeFalse(5 < 1);assertEquals(5 + 2, 7);}@Testvoid assumptionThat() {String someString = "Just a string";assumingThat(someString.equals("Just a string"),() -> assertEquals(2 + 2, 4));}
}
如果假设失败,则抛出TestAbortedException,并跳过测试。
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 {@Testvoid shouldThrowException() {Throwable exception = assertThrows(UnsupportedOperationException.class, () -> {throw new UnsupportedOperationException("Not supported");});assertEquals("Not supported", exception.getMessage());}@Testvoid 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 in = new ArrayList<>(Arrays.asList("Hello", "Yes", "No"));private List out = new ArrayList<>(Arrays.asList("Cześć", "Tak", "Nie"));@TestFactoryStream 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的大小。
上一篇:工具-Obsidian生产力工具,安装第三方插件(GitHub)教程,以安装Syntax Highlight(代码高亮)为例
下一篇:python画图技巧——画完一个Excel如何很方便的换另一个Excel来画或者画多个Excel综合处理的数据,以及换线条样式和颜色