美文网首页
JUnit 5 教程 之 基础篇

JUnit 5 教程 之 基础篇

作者: MrTT | 来源:发表于2020-02-15 19:36 被阅读0次

    JUnit 5 作为新一代的 Java 单元测试框架,提供很多改进。例如对比 JUnit4JUnit5 的官网,JUnit5 的设计更加简约与时尚,至少不会抗拒阅读的程度了(像破烂一样的网站,看了整个人都难受,不影响效率?不存在的)

    image

    而且,除此外,他的文档使用了 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),简单的勾选几个依赖


      image
    2. 添加 JUnit5 的依赖(spring boot 2.2 中已默认是Junit5,不需要额外加,详见WIKI),

        <dependency>
          <groupId>org.junit.jupiter</groupId>
          <artifactId>junit-jupiter</artifactId>
          <version>${latest-version}</version>
          <scope>test</scope>
        </dependency>
    

    org.junit.jupiter:junit-jupiter已包含了 JUnit Platform,不需要额外声明依赖,一个就够了

    image

    第一个测试用例

    1. 创建一个待测试的工具类
    public class TimeUtils {
        public static String hello(Instant now) {
            return "现在时间是:" + now.toString();
        }
    }
    
    1. 创建测试用例
    class TimeUtilsTest {
        @Test
        void hello() {
            Instant now = Instant.now();
            String expect = "现在时间是:" + now.toString();
            assertEquals(expect, TimeUtils.hello(now));
        }
    }
    
    1. 运行测试用例,如果你使用idea,那么直接点旁边的运行按钮,或者使用其它编辑器的功能测试,当然,你还可以选择通过命令行,下载junit-platform-console-standalone,并运行它(不懂),另一种是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<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());
                }
            }
        }
    }
    

    我们可以清晰的看到他们之间的关系

    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<String> stringProvider() {
            return Stream.of("apple", "banana");
        }
    
        @ParameterizedTest
        @MethodSource("stringIntAndListProvider")
        void testWithMultiArgMethodSource(String str, int num, List<String> list) {
            // 多参支持
            log.error(String.format("Content: %s is %d, %s", str, num, String.join(",", list)));
        }
        static Stream<Arguments> 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,如果转换失败,会寻找构造器或者静态构造方法(非私有)中,单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<String> dynamicTestsWithInvalidReturnType() {
            return Arrays.asList("Hello");
        }
    
        @TestFactory
        Collection<DynamicTest> dynamicTestsFromCollection() {
            return Arrays.asList(
                dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))),
                dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
            );
        }
    
        @TestFactory
        Iterable<DynamicTest> dynamicTestsFromIterable() {
            return Arrays.asList(
                dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))),
                dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
            );
        }
    
        @TestFactory
        Iterator<DynamicTest> 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<DynamicTest> dynamicTestsFromStream() {
            return Stream.of("racecar", "radar", "mom", "dad")
                .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
        }
    
        @TestFactory
        Stream<DynamicTest> 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<DynamicTest> generateRandomNumberOfTests() {
    
            // Generates random positive integers between 0 and 100 until
            // a number evenly divisible by 7 is encountered.
            Iterator<Integer> inputGenerator = new Iterator<Integer>() {
    
                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<Integer, String> displayNameGenerator = (input) -> "input:" + input;
    
            // Executes tests based on the current input value.
            ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);
    
            // Returns a stream of dynamic tests.
            return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
        }
    
        @TestFactory
        Stream<DynamicNode> 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 为何如此接近。。。

    本文作者: Mr.J
    本文链接: https://www.dnocm.com/articles/cherry/junit-5-info/
    版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

    相关文章

      网友评论

          本文标题:JUnit 5 教程 之 基础篇

          本文链接:https://www.haomeiwen.com/subject/xlbyfhtx.html