星语课程网
JUnit 5 教程三分钟教程
来源:简书
2023-03-03 08:24
375
JUnit 5 作为新一代的 Java 单元测试框架,提供很多改进。例如对比 [JUnit4](https://links.jianshu.com/go?to=https%3A%2F%2Fjunit.org%2Fjunit4%2F) 与 [JUnit5](https://links.jianshu.com/go?to=https%3A%2F%2Fjunit.org%2Fjunit5%2F) 的官网,JUnit5 的设计更加简约与时尚,至少不会抗拒阅读的程度了(像破烂一样的网站,看了整个人都难受,不影响效率?不存在的) 而且,除此外,他的文档使用了 Asciidoc, 相对于markdown复杂,主要是它还支持具有包含另一个文件内容,这对于写API文档来说挺重要的,有兴趣可以了解下~ Okay, 结束吐槽,让我来看看 JUnit5 到底带来了哪些变化吧 JUnit 5 是什么? ============ 与以往的版本不同,JUnit5 由三个模块模版组成 JUnit Platform + JUnit Jupiter + JUnit Vintage * JUnit Platform:运行测试框架的基础服务,定义了一套API,任何实现这套API的测试引擎,都能运行在这之上 * JUnit Jupiter:一系列用于编写JUnit5测试或者扩展的组合,同时他的子项目提供了JUnit5测试引擎 * JUnit Vintage:提供 JUnit3 和 JUnit4 的测试引擎 三分钟教程 ===== 环境搭建 ---- 1. 创建你的项目(建议Spring Boot),简单的勾选几个依赖  2. 添加 JUnit5 的依赖(spring boot 2.2 中已默认是Junit5,不需要额外加,详见[WIKI](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fspring-projects%2Fspring-boot%2Fwiki%2FSpring-Boot-2.2-Release-Notes%23junit-5)),
org.junit.jupiter
junit-jupiter
${latest-version}
test
`org.junit.jupiter:junit-jupiter`已包含了 JUnit Platform,不需要额外声明依赖,一个就够了  第一个测试用例 ------- 1. 创建一个待测试的工具类 public class TimeUtils { public static String hello(Instant now) { return "现在时间是:" + now.toString(); } } 2. 创建测试用例 class TimeUtilsTest { @Test void hello() { Instant now = Instant.now(); String expect = "现在时间是:" + now.toString(); assertEquals(expect, TimeUtils.hello(now)); } } 3. 运行测试用例,如果你使用idea,那么直接点旁边的运行按钮,或者使用其它编辑器的功能测试,当然,你还可以选择通过命令行,下载[junit-platform-console-standalone](https://links.jianshu.com/go?to=https%3A%2F%2Frepo1.maven.org%2Fmaven2%2Forg%2Fjunit%2Fplatform%2Fjunit-platform-console-standalone%2F),并运行它(不懂),另一种是`mvn test`运行测试 更多实用方案 ====== 别名 -- 测试的Class可以通过添加@DisplayName(),添加别名 @DisplayName("时间工具类测试") class TimeUtilsTest {} 也可以使用@DisplayNameGeneration(),进行更多的配置 @DisplayNameGeneration(TimeUtils2Test.ReplaceUnderscores.class) class TimeUtils2Test { @Test void hello() { Instant now = Instant.now(); String expect = "现在时间是:" + now.toString(); assertEquals(expect, TimeUtils.hello(now)); } static class ReplaceUnderscores extends DisplayNameGenerator.ReplaceUnderscores { @Override public String generateDisplayNameForClass(Class> testClass) { return "哈哈哈"; } } } 断言、假设 ----- 测试中核心之一,用于判断是否执行成功,在JUnit5中增加了些对lambdas的支持,例如: @Test void asserts() { assertEquals(1,2, () -> "1要是1"); } 另外,还增加了假设 @Test void assume() { assumingThat("DEV".equals(System.getenv("ENV")), () -> { // 如果不为true这里将不执行 assertEquals(1, 1); }); assumeTrue("DEV".equals(System.getenv("ENV")), () -> "Aborting test: not on developer workstation"); // 如果不为true这里将不执行 } 禁用 -- 添加@Disabled()可以禁用测试,这个意义在于某一测试用例遇到问题,临时不执行,等待问题修复后再次使用的 @Disabled("Disabled 因为重复") class TimeUtilsCopyTest {} 测试执行条件 ------ 通过添加 @EnabledOnOs 或者 @DisabledOnOs 来决定在某一操作系统上执行. @Test @EnabledOnOs(MAC) void testOnMac() { log.info("exec on mac"); } @Test @EnabledOnOs({ WINDOWS, LINUX }) void testOnOs() { log.info("exec on windows or linux"); } @EnabledOnJre 和 @DisabledOnJre 可以对java环境判断 @Test @EnabledOnJre(JRE.JAVA_8) void testOnJava8() { log.info("exec on java 8"); } @EnabledIfSystemProperty/@DisabledIfSystemProperty 与 @EnabledIfEnvironmentVariable/@DisabledIfEnvironmentVariable 分别判断系统和环境变量,他们的匹配项支持正则表达式 @Test @DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*") void notOnDeveloperWorkstation() { // ... } 标签/分组 ----- JUnit5 中支持通过 @Tag() 对测试用例进行分组,例如 @Tag("conditional") @Test @EnabledOnOs(MAC) void testOnMac() { log.info("exec on mac"); } @Tag("conditional") @Test @EnabledOnJre(JRE.JAVA_8) void testOnJava8() { log.info("exec on java 8"); } @Tag() 有以下这些语法规则 * 不能为null或者空字符串 * 不能有空格 * 不能包含ISO控制符 * 不能包含保留字符(`,`,`(`,`)`,`&`,`|`,`!`) 顺序 -- 添加@TestMethodOrder(MethodOrderer.OrderAnnotation.class)与@Order(),定义测试用例的执行顺序 @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class OrderedTest { @Test @Order(2) void emptyValues() { // perform assertions against empty values } @Test @Order(1) void nullValues() { // perform assertions against null values } @Test @Order(3) void validValues() { // perform assertions against valid values } } 生命周期 ---- JUnit5 提供了4个生命周期注解 @BeforeAll @AfterAll @BeforeEach @AfterEach * @BeforeAll:在所有的 @Test @RepeatedTest @ParameterizedTest @TestFactory 之前执行 * @BeforeEach:在每个测试用例前执行 * @AfterAll @AfterEach:与before类似,在测试用例之后执行 例如: @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class LifecycleTest { int num = 0; @BeforeAll static void initAll() { log.error("initAll"); } @BeforeEach void init() { log.error("init"); } @Test @Order(1) void doTest1() { log.error("num is " + num); num = 1; log.error("doTest1"); } @Test @Order(2) void doTest2() { log.error("num is " + num); num = 2; log.error("doTest1"); } } 除此外,还有@TestInstance()配置,见上面的例子,这个存在两个模式 * PER\_METHOD:每个测试用例执行前,都会创建一个实例(默认,与junit4一致) * PER\_CLASS:每个类的测试用例执行前,创建统一的实例 上面的例子中,得到的log为: 13:58:03.477 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - initAll 13:58:03.485 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - init 13:58:03.487 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - num is 0 13:58:03.487 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - doTest1 13:58:03.494 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - init 13:58:03.495 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - num is 1 13:58:03.495 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - doTest1 `doTest1()` 的执行,影响到num属性的值,而默认模式下则不会 嵌套 -- @Nested() 可以更好的表达测试用例间的关系,例如官方的例子 @DisplayName("A stack") class TestingAStackDemo { Stack
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()); } } } } 我们可以清晰的看到他们之间的关系  image 重复测试 ---- @RepeatedTest() 执行多次测试,支持name修改名称(具体见官网,觉得没多大意义),另外可以在方法中获取repetitionInfo参数,用于判断当前的执行情况(JUnit5支持注入参数,后续详说) @Slf4j class RepeatedTestsDemo { @RepeatedTest(2) void repeatedTest() { log.info("done!"); } @RepeatedTest(2) void repeatedTest2(RepetitionInfo repetitionInfo) { int currentRepetition = repetitionInfo.getCurrentRepetition(); int totalRepetitions = repetitionInfo.getTotalRepetitions(); log.info(String.format("About to execute repetition %d of %d", // currentRepetition, totalRepetitions)); } } 参数测试 ---- @ParameterizedTest 很实用的注解,需要`junit-jupiter-params`依赖(我们已经添加了) 它主要是配置@xxxSource,注入参数,以完成测试,参数的注入方式有多种 ### 数据源 @ValueSource 注入String内容,这是最常用的 @ParameterizedTest @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) void palindromes(String candidate) { log.error(candidate); } @EnumSource 注入枚举类 @ParameterizedTest @EnumSource(TimeUnit.class) void testWithEnumSource(TimeUnit timeUnit) { log.error(timeUnit.toString()); } @ParameterizedTest @EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" }) void testWithEnumSourceInclude(TimeUnit timeUnit) { // 选择部分 log.error(timeUnit.toString()); } @MethodSource 通过方法名注入(我更倾向于使用下面的@ArgumentsSource) @ParameterizedTest @MethodSource("stringProvider") void testWithExplicitLocalMethodSource(String argument) { log.error(argument); } static Stream
stringProvider() { return Stream.of("apple", "banana"); } @ParameterizedTest @MethodSource("stringIntAndListProvider") void testWithMultiArgMethodSource(String str, int num, List
list) { // 多参支持 log.error(String.format("Content: %s is %d, %s", str, num, String.join(",", list))); } static Stream
stringIntAndListProvider() { return Stream.of( arguments("apple", 1, Arrays.asList("a", "b")), arguments("lemon", 2, Arrays.asList("x", "y")) ); } @CsvSource csv源支持 @ParameterizedTest @CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 0xF1" }) void testWithCsvSource(String fruit, int rank) { log.error(fruit + rank); } 它也支持从文件导入,例如`@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)` @ArgumentsSource 通过自定义的参数提供器导入 @ParameterizedTest @ArgumentsSource(MyArgumentsProvider.class) void testWithArgumentsSource(String argument) { log.error(argument); } static class MyArgumentsProvider implements ArgumentsProvider { @Override public Stream extends Arguments> provideArguments(ExtensionContext context) { return Stream.of("apple", "banana").map(Arguments::of); } } ### 参数转换 为了支持csv,JUnit支持了些内建的转换,详细见文档[writing-tests-parameterized-tests-argument-conversion](https://links.jianshu.com/go?to=https%3A%2F%2Fjunit.org%2Fjunit5%2Fdocs%2Fcurrent%2Fuser-guide%2F%23writing-tests-parameterized-tests-argument-conversion),如果转换失败,会寻找构造器或者静态构造方法(非私有)中,单String的方法,来转换对应的对象 > 内建的转换有必要,但后一种,我宁愿得到报错,而不是转换,隐形的转换往往会导致莫名的问题出现 所以推荐通过@ConvertWith实现参数类型间的转换 @ParameterizedTest @ValueSource(strings = { "Wow,12", "radar,50"}) void toBook(@ConvertWith(ToBookConverter.class) Book book) { log.error(book.toString()); } static class ToBookConverter extends SimpleArgumentConverter { @Override protected Object convert(Object source, Class> targetType) { String value = String.valueOf(source); String[] split = value.split(","); return Book.of(split[0], Integer.parseInt(split[1])); } } JUnit中也内置了些转换,如@JavaTimeConversionPattern等 除外,还可以通过@AggregateWith转换或者接收ArgumentsAccessor对象 Dynamic测试 --------- 除了常规的@Test,我们还可以通过@TestFactory来构建整个测试树 class DynamicTestsDemo { private final Calculator calculator = new Calculator(); // This will result in a JUnitException! @TestFactory List
dynamicTestsWithInvalidReturnType() { return Arrays.asList("Hello"); } @TestFactory Collection
dynamicTestsFromCollection() { return Arrays.asList( dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ); } @TestFactory Iterable
dynamicTestsFromIterable() { return Arrays.asList( dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ); } @TestFactory Iterator
dynamicTestsFromIterator() { return Arrays.asList( dynamicTest("5th dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("6th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ).iterator(); } @TestFactory DynamicTest[] dynamicTestsFromArray() { return new DynamicTest[] { dynamicTest("7th dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("8th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) }; } @TestFactory Stream
dynamicTestsFromStream() { return Stream.of("racecar", "radar", "mom", "dad") .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))); } @TestFactory Stream
dynamicTestsFromIntStream() { // Generates tests for the first 10 even integers. return IntStream.iterate(0, n -> n + 2).limit(10) .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0))); } @TestFactory Stream
generateRandomNumberOfTests() { // Generates random positive integers between 0 and 100 until // a number evenly divisible by 7 is encountered. Iterator
inputGenerator = new Iterator
() { Random random = new Random(); int current; @Override public boolean hasNext() { current = random.nextInt(100); return current % 7 != 0; } @Override public Integer next() { return current; } }; // Generates display names like: input:5, input:37, input:85, etc. Function
displayNameGenerator = (input) -> "input:" + input; // Executes tests based on the current input value. ThrowingConsumer
testExecutor = (input) -> assertTrue(input % 7 != 0); // Returns a stream of dynamic tests. return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor); } @TestFactory Stream
dynamicTestsWithContainers() { return Stream.of("A", "B", "C") .map(input -> dynamicContainer("Container " + input, Stream.of( dynamicTest("not null", () -> assertNotNull(input)), dynamicContainer("properties", Stream.of( dynamicTest("length > 0", () -> assertTrue(input.length() > 0)), dynamicTest("not empty", () -> assertFalse(input.isEmpty())) )) ))); } @TestFactory DynamicNode dynamicNodeSingleTest() { return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop"))); } @TestFactory DynamicNode dynamicNodeSingleContainer() { return dynamicContainer("palindromes", Stream.of("racecar", "radar", "mom", "dad") .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))) )); } } 还未看过源码,但目测@Test是由内建的转换器,转换成DynamicNode,然后再执行。使用@TestFactory,tree型的代码也是种选择,再维护上,不差于@Test的常规方案 > 后续还有扩展与Spring应用篇,欢迎关注我哦~ 一个小疑问,JUnit5 的注解风格和 Spring 为何如此接近。。。 * 例子源码:[https://github.com/jiangtj-lab/junit5-demo](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fjiangtj-lab%2Fjunit5-demo) > **本文作者:** Mr.J > **本文链接:** [https://www.dnocm.com/articles/cherry/junit-5-info/](https://links.jianshu.com/go?to=https%3A%2F%2Fwww.dnocm.com%2Farticles%2Fcherry%2Fjunit-5-info%2F) > **版权声明:** 本博客所有文章除特别声明外,均采用 [BY-NC-SA](https://links.jianshu.com/go?to=https%3A%2F%2Fcreativecommons.org%2Flicenses%2Fby-nc-sa%2F4.0%2F) 许可协议。转载请注明出处!
点赞
热门评论
最新评论
匿名用户
+1
-1
·
回复TA
暂无热门评论
相关推荐
阅读更多资讯
热门评论 最新评论
暂无热门评论