Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库
基础部分
JUnit 5 介绍 JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform:Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。 JUnit Jupiter:JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于在Junit Platform上运行。 JUnit Vintage:提供了一个TestEngine
用于在平台上运行基于 JUnit 3 和 JUnit 4 的测试(兼容之前的老版本)。它要求类路径或模块路径上存在 JUnit 4.12 或更高版本 SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容JUnit4需要自行引入(不能使用JUnit4的功能 @Test)
JUnit 5’s Vintage 已经从spring-boot-starter-test
从移除。如果需要继续兼容Junit4需要自行引入Vintage依赖:
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > org.junit.vintage</groupId > <artifactId > junit-vintage-engine</artifactId > <scope > test</scope > <exclusions > <exclusion > <groupId > org.hamcrest</groupId > <artifactId > hamcrest-core</artifactId > </exclusion > </exclusions > </dependency >
依赖 使用JUnit 5,对应的依赖
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency >
Spring的JUnit 5的基本单元测试模板【Spring的JUnit4的是 @SpringBootTest
+ @RunWith(SpringRunner.class)
】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import org.junit.jupiter.api.Assertions;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest class SpringBootApplicationTests { @Autowired private Component component; @Test public void contextLoads () { Assertions.assertEquals(5 , component.getFive()); } }
常用测试注解 JUnit Jupiter supports the following annotations for configuring tests and extending the framework.
注释 说明 @Test 表示方法是测试方法 @ParameterizedTest 表示方法是参数化测试 @RepeatedTest 表示方法将会重复执行 @DisplayName 声明测试类或测试方法的自定义显示名称 @BeforeEach 表示被注解的方法应该在当前类中的每个方法之前执行 @AfterEach 表示被注解的方法应该在当前类中的每个方法之后执行 @BeforeAll 表示被注解的方法应该在当前类中的所有方法之前执行 @AfterAll 表示被注解的方法应该在当前类中的所有方法之后执行 @Tag 用于在类或方法级别声明用于过滤测试的标签 @Disabled 用于禁用测试类或测试方法,不执行 @Timeout 表示测试方法运行如果超过了指定时间将会返回错误 @ExtendWith 为测试类或测试方法提供扩展类引用
实例代码
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 import org.junit.jupiter.api.*;import org.junit.jupiter.params.ParameterizedTest;import org.junit.jupiter.params.provider.ValueSource;import java.util.concurrent.TimeUnit;import static org.junit.jupiter.api.Assertions.assertTrue;@DisplayName("junit5功能测试类") public class Junit5Test { @DisplayName("测试方法1:参数化测试") @ParameterizedTest @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) void palindromes (String candidate) { boolean isPalindrome = isPalindrome(candidate); assertTrue(isPalindrome, "String is not a palindrome: " + candidate); } private boolean isPalindrome (String str) { str = str.replaceAll("\\s+" , "" ); str = str.toLowerCase(); int left = 0 ; int right = str.length() - 1 ; while (left < right) { if (str.charAt(left) != str.charAt(right)) { return false ; } left++; right--; } return true ; } @Disabled @DisplayName("测试方法2:不执行") @Test void test2 () { System.out.println("方法存在bug,不执行" ); } @RepeatedTest(5) @DisplayName("测试方法3:重复执行") @Test void test3 () { System.out.println("方式将会重复执行!" ); } @DisplayName("测试方法4:超时异常") @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) @Test void testTimeout () throws InterruptedException { Thread.sleep(600 ); } @BeforeEach void testBeforeEach (TestInfo testInfo) { String testName = testInfo.getDisplayName(); System.out.println("测试方法 " + testName + " 开始..." ); } @AfterEach void testAfterEach (TestInfo testInfo) { String testName = testInfo.getDisplayName(); System.out.println("测试方法 " + testName + " 结束 ..." ); } @BeforeAll static void testBeforeAll () { System.out.println("所有测试就要开始了..." ); } @AfterAll static void testAfterAll () { System.out.println("所有测试以及结束了..." ); } }
断言机制 断言Assertion是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions
的静态方法。检查业务逻辑返回的数据是否合理。
JUnit 5 内置的断言可以分成如下几个类别:
简单断言 用来对单个值进行简单的验证。如:
方法 说明 assertEquals 判断两个对象或两个原始类型是否相等 assertNotEquals 判断两个对象或两个原始类型是否不相等 assertSame 判断两个对象引用是否指向同一个对象 assertNotSame 判断两个对象引用是否指向不同的对象 assertTrue 判断给定的布尔值是否为 true assertFalse 判断给定的布尔值是否为 false assertNull 判断给定的对象引用是否为 null assertNotNull 判断给定的对象引用是否不为 null
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 import org.junit.jupiter.api.Assertions;import org.junit.jupiter.api.DisplayName;import org.junit.jupiter.api.Test;import org.springframework.boot.test.context.SpringBootTest;@DisplayName("简单断言测试") @SpringBootTest public class SpringBoot_Assert { @Test @DisplayName("测试简单断言1:assertEquals") void testSimpleAssertions1 () { Assertions.assertEquals(3 , 1 + 5 , "原始类型是否相等" ); Assertions.assertNotEquals(3 , 1 + 1 ); } @Test @DisplayName("测试简单断言2:assertSame") void testSimpleAssertions2 () { Object obj = new Object(); Assertions.assertNotSame(new Object(), new Object(), "是否指向同一个对象" ); Assertions.assertSame(obj, obj); } @Test @DisplayName("测试简单断言3:assertTrue") void testSimpleAssertions3 () { Assertions.assertTrue(1 > 2 , "布尔值是否为 true" ); Assertions.assertFalse(1 > 2 ); } @Test @DisplayName("测试简单断言4:assertTrue、assertNull") void testSimpleAssertions4 () { Assertions.assertNull(null , "对象引用是否为 null" ); Assertions.assertNotNull(new Object()); } }
数组断言 通过 assertArrayEquals
方法来判断两个对象或原始类型的数组是否相等。
1 2 3 4 5 @Test @DisplayName("数组断言") public void array () { assertArrayEquals(new int []{1 , 3 }, new int [] {1 , 2 },"array contents differ!!!" ); }
组合断言 assertAll()
方法接受多个 org.junit.jupiter.api.Executable
函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言。
1 2 3 4 5 6 7 8 @Test @DisplayName("组合断言测试") public void all () { assertAll("Math" , () -> assertEquals(2 , 3 ,"not equals!!!" ), () -> assertTrue(1 > 0 ) ); }
异常断言 JUnit5提供了一种新的断言方式Assertions.assertThrows()
,配合函数式编程就可以进行使用,用来期望捕获这个特定的异常
1 2 3 4 5 6 7 @Test @DisplayName("异常测试") public void exceptionTest () { Assertions.assertThrows( ArithmeticException.class, () -> System.out.println(1 % 2 ),"未捕获到算术异常:" ); }
超时断言 JUnit5还提供了Assertions.assertTimeout()为测试方法设置了超时时间。
1 2 3 4 5 6 @Test @DisplayName("超时测试") public void timeoutTest () { Assertions.assertTimeout(Duration.ofMillis(1000 ), () -> Thread.sleep(2000 )); }
快速失败 通过 fail 方法直接使得测试失败。
1 2 3 4 5 @Test @DisplayName("fail") public void shouldFail () { fail("This should fail" ); }
单元测试 前置条件 Unit 5 中的前置条件类似于断言,不同之处在于不满足的断言assertions 会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止 。
前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要(assumingThat
方法接受两个参数。第一个参数是一个判读条件,如果满足这个条件,才会执行第二个参数中的操作)
assumeTrue
和 assumFalse
确保给定的条件为 true
或 false
,不满足条件会使得测试执行终止。
assumingThat
方法接受两个参数,分别是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable
对象才会被执行;当条件不满足时,测试执行并不会终止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test @DisplayName("assume then do") public void assumeThenDo () { assumingThat( Objects.equals("DEV" , "TEST" ), () -> System.out.println("In DEV" ) ); } @Test @DisplayName("simple") public void simpleAssume () { assumeTrue(()-> Objects.equals("DEV" , "PROD" ),"assumeTrue()不满足条件,不继续执行" ); assumeFalse(() -> Objects.equals("DEV" , "DEV" ),"assumeFalse()不满足条件,不继续执行" ); }
嵌套测试 官方文档 - Nested Tests
JUnit 5 可以通过 Java 中的内部类和@Nested
注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起 。在内部类中可以使用@BeforeEach
和@AfterEach
注解,而且嵌套的层次没有限制。
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 @DisplayName("A stack") class TestingAStackDemo { Stack<Object> 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()); } } } }
参数化测试 官方文档 - Parameterized Tests
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
@ValueSource : 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型@NullSource : 表示为参数化测试提供一个null的入参@EnumSource : 表示为参数化测试提供一个枚举入参@CsvFileSource :表示读取指定CSV文件内容作为参数化测试入参@MethodSource :表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV、YML、JSON 文件甚至方法的返回值也可以作为入参。只需要去实现**ArgumentsProvider
**接口,任何外部文件都可以作为它的入参。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @ParameterizedTest @ValueSource(strings = {"one", "two", "three"}) @DisplayName("参数化测试1") public void parameterizedTest1 (String string) { System.out.println(string); Assertions.assertTrue(StringUtils.isNotBlank(string)); } @ParameterizedTest @MethodSource("getFruit") @DisplayName("方法来源参数") public void testWithExplicitLocalMethodSource (String name) { System.out.println(name); Assertions.assertNotNull(name); } static Stream<String> getFruit () { return Stream.of("apple" , "banana" ); }
迁移指南 官方文档 - Migrating from JUnit 4
在进行迁移的时候需要注意如下的变化:
注解在 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
。