参考资料:
0. JUnit 4 用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import org.junit.Before;import org.junit.Test;import java.util.UUID;public class Demo01 { String uuid; @Before public void init () { uuid = UUID.randomUUID().toString().replace("-" , "" ); } @Test public void test () { System.out.println("随机码 = " + uuid); } }
1. JUnit 5 架构 Junit5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
使用 JUnit Jupiter 编写测试内容 JUnit Jupiter 包含两个组件:API
和 Test Engine
。
使用 API ( 通过注解、断言、回调等) 创建
单元测试。
使用 Test Engine 发现和执行
JUnit Jupiter 单元测试。
JUnit Platform 包含 API、Test Engine 和 Launcher。
运行单元测试的过程分为两部分:
发现测试和创建测试计划
提供由一个 TestEngine 实现的,用于发现测试和创建测试计划的 API
使用 IDE 和构建工具发起测试发现流程
根据测试规范创建测试计划
启动测试计划,以执行测试和向用户报告结果
提供由一个或多个 TestEngine 实现的,用于执行测试的 API
通过 IDE 和构建工具发起测试执行工作
Launcher 组件负责执行在测试发现期间创建的测试计划
后向兼容性:JUnit Vintage JUnit Vintage 可确保现有 JUnit 测试能与使用 JUnit Jupiter 创建的新测试一同运行。
JUnit Vintage 本身由两个模块组成:
junit:junit 是用于 JUnit 3 和 JUnit 4 的 API。
junit-vintage-engine:是在 JUnit Platform 上运行 JUnit 3 和 JUnit 4 测试的测试引擎。
2. JUnit Jupiter 编写 注解 JUnit 4 与 JUnit 5 中的常用注解比较
JUnit 5
JUnit 4
说明
@Test
@Test
被注解的方法是一个测试方法。与 JUnit 4 相同。
@BeforeAll
@BeforeClass
被注解的(静态)方法将在当前类中的所有 @Test 方法前执行一次。
@AfterAll
@AfterClass
被注解的(静态)方法将在当前类中的所有 @Test 方法后执行一次。
@BeforeEach
@Before
被注解的方法将在当前类中的每个 @Test 方法前执行。
@AfterEach
@After
被注解的方法将在当前类中的每个 @Test 方法后执行。
@Disabled
@Ignore
被注解的方法不会执行(将被跳过),但会报告为已执行。
@ExtendWith
@RunWith
放在测试类名之前,用来确定这个类怎么运行的
@ExtendWith
@Rule
一组实现了TestRule接口的共享类
@ExtendWith
@ClassRule
用于测试类中的静态变量,必须是TestRule接口的public实例
@Tag
@Category
被用于通过声明标签来过滤测试方法
@TestFactory
声明这个方法是针对于dynamic tests测试工厂
@DisplayName
给这个类或者方法设定一个特殊的名字
@Nested
声明这个方法是 一个嵌套的, 非静态的方法
使用注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @RunWith(JUnitPlatform.class) @DisplayName("Testing using JUnit 5") public class JUnit5AppTest { private static final Logger log = LoggerFactory.getLogger(JUnit5AppTest.class); private App classUnderTest; @BeforeAll public static void init () { } @AfterAll public static void done () { } @BeforeEach public void setUp () throws Exception { classUnderTest = new App (); } @AfterEach public void tearDown () throws Exception { classUnderTest = null ; } @Test @DisplayName("Dummy test") void aTest () { log.info("As written, this test will always pass!" ); assertEquals(4 , (2 + 2 )); } @Test @Disabled @DisplayName("A disabled test") void testNotRun () { log.info("This test will not run (it is disabled, silly)." ); } . . }
断言 Junit Jupiter 继承了许多 Junit 4 中的断言方法,同时增加了一些适配 Java 8 lambdas 特点的方法。所有的 Junit Jupiter 都是静态方法,在 org.junit.jupiter.Assertions 类中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import static org.junit.jupiter.api.Assertions . assertEquals; import static org.junit.jupiter.api.Assertions . assertFalse; import static org.junit.jupiter.api.Assertions . assertNotNull; import static org.junit.jupiter.api.Assertions . assertNull; import static org.junit.jupiter.api.Assertions . assertTrue; . . @Test @DisplayName("Dummy test" ) void dummyTest() { int expected = 4 ; int actual = 2 + 2 ; assert Equals(expected , actual , "INCONCEIVABLE!" ) ; Object nullValue = null; assert False(nullValue != null ) ; assert Null(nullValue ) ; assert NotNull("A String" , "INCONCEIVABLE!" ) ; assert True(nullValue == null ) ; . . }
@assertAll() assertAll() 包含的所有断言都会执行,即使一个或多个断言失败也是如此。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import static org.junit.jupiter.api.Assertions.assertAll; . . @Test @DisplayName("Dummy test" )void dummyTest() { int expected = 4 ; int actual = 2 + 2 ; Object nullValue = null ; . . assertAll( "Assert All of these" , () -> assertEquals(expected, actual, "INCONCEIVABLE!" ), () -> assertFalse(nullValue != null ), () -> assertNull(nullValue), () -> assertNotNull("A String" , "INCONCEIVABLE!" ), () -> assertTrue(nullValue == null )); }
@assertThrows() 在某些条件下,接受测试的类应抛出异常。JUnit 4 通过 expected = 方法参数或一个 @Rule 提供此能力。与此相反,JUnit Jupiter 通过 Assertions 类提供此能力,使它与其他断言更加一致。
1 2 3 4 5 6 7 8 9 10 import static org.junit.jupiter.api.Assertions.assertThrows;import static org.junit.jupiter.api.Assertions.assertEquals; . . @Test() @DisplayName("Empty argument" ) public void testAdd_ZeroOperands_EmptyArgument() { long[] numbersToSum = {}; assertThrows(IllegalArgumentException.class , () -> classUnderTest.add(numbersToSum)) ;}
前置条件 前置条件 (Assumption) 与断言类似,但前置条件必须为 true,否则测试将中止。
前置条件是 org.junit.jupiter.api.Assumptions 类的静态方法。
assumeTrue() 如果条件不成立,就不会执行 lambda 表达式的内容。
1 2 3 4 5 6 7 @Test @DisplayName("This test is only run on Fridays" ) public void testAdd_OnlyOnFriday() { LocalDateTime ldt = LocalDateTime . now() ; assumeTrue(ldt .getDayOfWeek () .getValue() == 5 ); }
assumingThat() 无论 assumingThat() 中的前置条件成立与否,都会执行 lambda 表达式后的所有代码
1 2 3 4 5 6 7 8 9 10 @Test @DisplayName("This test is only run on Fridays (with lambda)") public void testAdd_OnlyOnFriday_WithLambda () { LocalDateTime ldt = LocalDateTime.now(); assumingThat(ldt.getDayOfWeek().getValue() == 5 , () -> { }); }
嵌套测试 只有非静态的嵌套类可以被标记为 @Nested 测试,嵌套可以是任意的深度。
每个单元测试可以拥有自己的测试前和测试后生命周期,除了一个例外: @BeforeAll 和 @AfterAll 不起作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RunWith (JUnitPlatform.class)@DisplayName ("Testing JUnit 5" ) public class JUnit5AppTest { . . @Nested @DisplayName ("When zero operands" ) class JUnit5AppZeroOperandsTest { } . . }
在 IDE 中运行单元测试
1 2 3 @RunWith(JUnitPlatform.class )public class JUnit5AppTest { }
使用 Maven 运行单元测试
4. JUnit Jupiter 扩展 扩展 JUnit 4 的核心功能 使用 Runner 和 @Rule 扩展。
Runner 必须在测试类级别上使用 @RunWith 注解来声明 Runner,每个测试类最多只能拥有一个 Runner。
常见的第三方Runner ,比如用于运行基于 Spring 的单元测试的 SpringJUnit4ClassRunner,以及用于处理单元测试中 Mockito 对象的 MockitoJUnitRunner。
@Rule 为了解决 Runner 概念的这一内置限制,JUnit 4.7 引入了 @Rule。
一个测试类可声明多个 @Rule,这些规则可在测试方法级别和类级别上运行
扩展点和测试生命周期 一个扩展点对应于 JUnit test 生命周期中一个预定义的点。
接口
说明
AfterAllCallback
定义 API 扩展,希望在调用所有测试后让测试容器执行额外的行为。
AfterEachCallback
定义 API 扩展,希望在调用每个测试方法后让测试执行额外的行为。
AfterTestExecutionCallback
定义 API 扩展,希望在执行每个测试后让测试立即执行额外的行为。
BeforeAllCallback
定义 API 扩展,希望在调用所有测试前让测试容器执行额外的行为。
BeforeEachCallback
定义 API 扩展,希望在调用每个测试前让测试执行额外的行为。
BeforeTestExecutionCallback
定义 API 扩展,希望在执行每个测试前让测试立即执行额外的行为。
ParameterResolver
定义 API 扩展,希望在运行时动态解析参数。
TestExecutionExceptionHandler
定义 API 扩展,希望处理在测试执行期间抛出的异常。
激活扩展 要激活上述扩展,只需使用 @ExtendWith 注解注册它
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @ExtendWith (MyBeforeEachCallbackExtension.class) public class MyTestClass { . . @Test public void myTestMethod () { } @Test public void someOtherTestMethod() { } . . }
参数注入 将一个参数传递给 @Test 方法
ParameterResolver 接口 ParameterResolver 接口包含 2 个方法:
supports() 方法:测试引擎解析测试类参数时,首先会调用 supports() 方法,查看该扩展是否能处理这种参数类型。
resolve() 方法:如果 supports() 返回 true,则调用 resolve() 来获取正确类型的 Object,随后在调用测试方法时会使用该对象。
创建 ParameterResolver 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class GeneratedPersonParameterResolver implements ParameterResolver { @Override public boolean supportsParameter (ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { return parameterContext.getParameter().getType() == Person.class; } @Override public Object resolveParameter (ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { return PersonGenerator.createPerson () ; } }
使用 ParameterResolver 实现 在类或方法上,使用 @ExtendWith 注解完成注册工作
1 @ExtendWith(GeneratedPersonParameterResolver.class )
参数化测试 参数化测试是指多次调用 @Test 方法,但每次都使用不同的参数值。参数化测试必须使用 @ParameterizedTest 进行注解,而且必须为其参数指定一个来源。
JUnit Jupiter 提供了多个来源。每个来源指定一个 @ArgumentsSource,也就是一个 ArgumentsProvider 实现。
@ValueSource 仅支持以下类型:String、int、long、double
1 2 3 4 5 6 7 8 @ParameterizedTest @ValueSource(longs = { 1L, 2L, 3L, 4L, 5L }) public void findById(Long id ) { assert NotNull(classUnderTest ) ; Person personFound = classUnderTest.findById(id ) ; assert NotNull(personFound ) ; assert Equals(id , personFound .getId () ); }
@EnumSource 1 2 3 4 5 6 7 8 9 10 @ParameterizedTest @EnumSource(PersonTestEnum.class ) public void findById(PersonTestEnum testPerson ) { assert NotNull(classUnderTest ) ; Person person = testPerson.getPerson() ; Person personFound = classUnderTest.findById(person .getId () ); assert NotNull(personFound ) ; performPersonAssertions(person .getLastName () , person.getFirstName() , person.getAge() , person.getEyeColor() , person.getGender() , personFound); }
@MethodSource 一个方法来源必须声明为 static,返回类型必须是 Stream、Iterator、Iterable 或数组。
1 2 3 4 5 6 7 8 9 10 11 @ParameterizedTest @MethodSource(value = "personProvider" ) public void findById(Person paramPerson ) { assert NotNull(classUnderTest ) ; long id = paramPerson.getId() ; Person personFound = classUnderTest.findById(id ) ; assert NotNull(personFound ) ; performPersonAssertions(paramPerson .getLastName () , paramPerson.getFirstName() , paramPerson.getAge() , paramPerson.getEyeColor() , paramPerson.getGender() , personFound); }
自定义显示名称 可以通过向 @ParameterizedTest 注解提供任何以下属性值来自定义输出:
{index}:从 1 开始的索引(当前测试迭代 )。
{arguments}:完整的参数列表,使用逗号分隔。
{0}, {1} …:一个特定的参数(0 是第一个,依此类推)。
1 @ParameterizedTest(name = "@ValueSource: FindById(): Test# {index}: Id: {0}")
动态测试 @TestFactory @TestFactory 方法用于生成动态测试。此方法必须返回 DynamicTest 实例的 Stream、Collection、Iterable 或 Iterator。
创建 @TestFactory 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @TestFactory Collection<DynamicTest> dynamicTestsFromCollection() { return Arrays.asList( dynamicTest("1st dynamic test" , () -> assertTrue(true )) , dynamicTest ("2nd dynamic test" , () -> assertEquals(4 , 2 * 2 )) ); } @TestFactory Stream <DynamicTest > dynamicTestsFromStream () { return Stream .of ("A" , "B" , "C" ) .map ( str -> dynamicTest("test" + str, () -> { /* ... */ })) ;}
标签和过滤 可使用标签来注解方法或类。然后可使用 Maven POM 或 Gradle 构建脚本中的过滤器设置来过滤掉此测试。
使用 Maven 过滤 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <build > <plugins > . . <plugin > <artifactId > maven-surefire-plugin</artifactId > <version > 2.19</version > <configuration > <properties > <excludeTags > advanced</excludeTags > </properties > </configuration > . . </plugins > </build >
使用 Gradle 过滤 1 2 3 4 5 6 7 8 9 10 junitPlatform { filters { engines { } tags { exclude 'advanced ' } } logManager 'org .apache.logging.log4j.jul.LogManager' }