优雅的单测-Junit5

一、背景

单测定义

是开发提测前研发自测的一种手段和方式,用一段代码检测业务功能是否按照需求case执行

为什么要单测

起源

在敏捷开发中的有一项核心实践和技术设计方法论,TDD(测试驱动开发)

TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码

好处

保障代码最有效的办法(还有CR)
降本增效,高效协作,减少BUG量

目的

验证行为(是否如期执行,异常是否处理)
为设计铺垫(提前思考系统的设计)
自动化回归(快速简单的运行)

单测规范

AIR(单元测试在线上运行时,感觉像空气(AIR)一样感觉不到,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点)

规范说明
A(Automatic自动化 )全程自动化 无需人肉验证 断言
(Independent独立性)单元测试之前独立运行 不依赖 无先后顺序
R(Repeatable可重复)可重复执行不受环境影响

注意事项:质量、实施方案、运行时长

二、Testing框架

TestNg 借鉴于Junit但功能更加强大的测试框架(集成测试、依赖测试)

Junit 主流单测框架
在这里插入图片描述
部分功能对比
在这里插入图片描述
注解差异
在这里插入图片描述

三、Junit5

Junit5整体结构
在这里插入图片描述
添加Maven坐标

<!--		Junit5-->
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-launcher</artifactId>
            <version>1.3.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.7.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
            <version>5.7.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>5.7.2</version>
            <scope>test</scope>
        </dependency>
        <!--		Junit5-->

简单测试

@DisplayName("简单测试")
public class SimpleTest {

    @BeforeAll
    public static void beforeClass() {
        System.out.println("-------before class execute-------");
    }
    @BeforeEach
    public  void beforeTest() {
        System.out.println("》》》》》》》before Test execute《《《《《《《");
    }


    @Test
    @DisplayName("测试加法")
    public void testSum(){
        System.out.println("测试加法");
        Assertions.assertEquals(2, Calculator.sum(1,1));
    }

    @Test
    @DisplayName("测试减法")
    public void testSub(){
        System.out.println("测试减法");
        Assertions.assertEquals(0, Calculator.sub(1,1));
    }

    @Test
    @DisplayName("测试乘法")
    public void testMul(){
        System.out.println("测试乘法");
        Assertions.assertEquals(1, Calculator.mul(1,1));
    }

    @Test
    @DisplayName("测试除法")
    public void testDiv(){
        System.out.println("测试除法");
        Assertions.assertEquals(1, Calculator.div(1,1));
    }



    @AfterAll
    public static void afterClass() {
        System.out.println("-------after class execute-------");
    }
    @AfterEach
    public  void afterTest() {
        System.out.println("》》》》》》》after Test execute《《《《《《《");
    }
}

依赖注入

@DisplayName("依赖注入测试")
public class DITest {

    DITest(TestInfo testInfo) {
        System.out.println("实例化的时候注入一次");
        Assertions.assertEquals("依赖注入测试", testInfo.getDisplayName());
    }

    @BeforeEach
    public void beforeTest(TestInfo testInfo) {
        String displayName = testInfo.getDisplayName();
        assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
    }

    @Test
    @DisplayName("TEST 1")
    @Tag("my-tag")
    void test1(TestInfo testInfo) {
        Assertions.assertEquals("TEST 1", testInfo.getDisplayName());
        assertTrue(testInfo.getTags().contains("my-tag"));
    }

    @Test
    void test2() {}



    @DisplayName("testReporter 1")
    @Test
    void reportSingleValue(TestReporter testReporter) {
        testReporter.publishEntry("a key", "a value");
    }

    @DisplayName("testReporter 2")
    @Test
    void reportSeveralValues(TestReporter testReporter) {
        HashMap<String, String> values = new HashMap<>();
        values.put("name", "july");
        values.put("year", "1993");
        testReporter.publishEntry(values);
    }
}

断言测试

@DisplayName("断言测试")
public class AssertTest {

    @Test
    @DisplayName("测试断言equals")
    void testEquals() {
        assertTrue(3 < 4);
    }

    @Test
    @DisplayName("测试断言NotNull")
    void testNotNull() {
        assertNotNull(new Object());
    }

    @Test
    @DisplayName("测试断言抛异常")
    void testThrows() {
        ArithmeticException arithExcep = assertThrows(ArithmeticException.class, () -> {
            int m = 5/0;
        });
        assertEquals("/ by zero", arithExcep.getMessage());
    }

    @Test
    @DisplayName("测试断言超时")
    void testTimeOut() {
        String actualResult = assertTimeout(ofSeconds(2), () -> {
            Thread.sleep(1000);
            return "a result";
        });
        assertEquals("a result", actualResult);
    }

    @Test
    @DisplayName("测试组合断言")
    void testAll() {
        assertAll("测试商城下单",
                () -> assertTrue(2 < 2, "库存不足"),
                () -> assertTrue(1 < 2,"余额不足"),
                () -> assertNotNull(new Object(),"交易异常")
        );
    }

}

嵌套测试

@DisplayName("嵌套测试")
public class NestedTest {

    @BeforeAll
    public static void beforeClass() {
        System.out.println("-------before class execute-------");
    }
    @BeforeEach
    public  void beforeTest() {
        System.out.println("》》》》》》》before Test execute《《《《《《《");
    }

    @Nested
    class CalculateDemo {

        @AfterEach
        @DisplayName("afterCalculateDemo")
        public  void afterTest() {
            System.out.println("afterCalculateDemo........");
        }

        @Test
        @DisplayName("testCalculateDemo")
        public void testCalculateDemo(){
            System.out.println("testCalculateDemo........");
            Assertions.assertEquals(6, Calculator.sum(3, 3));
        }
    }


    @AfterAll
    public static void afterClass() {
        System.out.println("-------after class execute-------");
    }
    @AfterEach
    public  void afterTest() {
        System.out.println("》》》》》》》after Test execute《《《《《《《");
    }
}

超时测试

@DisplayName("执行超时验证")
public class TimeOutTest {

    @Test
    @DisplayName("执行超时失败验证")
    void testFailWithTimeout() throws InterruptedException {
        Assertions.assertTimeout(Duration.ofMillis(100), () -> Thread.sleep(100));
    }

    @Test
    @DisplayName("执行超时成功验证")
    void testSuccessWithTimeout() throws InterruptedException {
        Assertions.assertTimeout(Duration.ofMillis(100), () -> Thread.sleep(99));
    }
}

异常测试

@DisplayName("异常测试")
public class ExceptionTest {

    @Test
    @DisplayName("测试抛出RunTimeException")
    void testThrowsException2(){
        Assertions.assertThrows(RuntimeException.class, () -> {
            throw new RuntimeException();
        });
    }

}

参数化测试

@DisplayName("参数化测试")
public class ParameterizedTestDemo {

    @DisplayName("测试ValueSource参数One")
    @ParameterizedTest
    @ValueSource(strings = {"111", "222", "I am july"})
    public void testWithValueSourceOne(String candidate) {
        System.out.println(candidate);
        Assertions.assertTrue(StringUtils.isNumeric(candidate));
    }

    @DisplayName("测试ValueSource参数Two")
    @ParameterizedTest
    @ValueSource(ints = {111,222,333})
    public void testWithValueSourceTwo(int argument) {
        System.out.println(argument);
        Assertions.assertEquals(argument % 111, 0);
    }

    /**
     * @ EnumSource能够很方便地提供Enum常量。该注解提供了一个可选的names参数,
     * 你可以用它来指定使用哪些常量。如果省略了,就意味着所有的常量将被使用
     */
    @DisplayName("测试ValueSource枚举参数All")
    @ParameterizedTest
    @EnumSource(TimeUnit.class)
    void testWithEnumSourceEnumAll(TimeUnit timeUnit) {
        System.out.println(timeUnit.name());
        Assertions.assertNotNull(timeUnit);
    }

    @DisplayName("测试ValueSource枚举参数Part")
    @ParameterizedTest
    @EnumSource(value = TimeUnit.class, names = {"DAYS", "HOURS"})
    void testWithEnumSourceEnumPart(TimeUnit timeUnit) {
        System.out.println(timeUnit.name());
        Assertions.assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
    }

    /**
     * @ EnumSource注解还提供了一个可选的mode参数,它能够细粒度地控制哪些常量将会被传递到测试方法中。例如,
     * 你可以从枚举常量池中排除一些名称或者指定正则表达式,如下面代码所示。
     */
    @DisplayName("测试ValueSource枚举参数Exclude")
    @ParameterizedTest
    @EnumSource(value = TimeUnit.class, mode = EnumSource.Mode.EXCLUDE, names = {"DAYS", "HOURS"})
    void testWithEnumSourceExclude(TimeUnit timeUnit) {
        assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
        Assertions.assertTrue(timeUnit.name().length() > 5);
    }


    /**
     * @ MethodSource允许你引用测试类中的一个或多个工厂方法。这些工厂方法必须返回一个Stream、Iterable、
     * Iterator或者参数数组。另外,它们不能接收任何参数。默认情况下,它们必须是static方法,除非测试类使用了
     * @ TestInstance(Lifecycle.PER_CLASS)注解。
     */
    @DisplayName("测试MethodSource参数One")
    @ParameterizedTest
    @MethodSource("stringProvider")
    void testWithSimpleMethodSourceOne(String argument) {
        System.out.println(argument);
        Assertions.assertNotNull(argument);
    }
    static Stream<String> stringProvider() {
        return Stream.of("张三", "李四");
    }

    @DisplayName("测试MethodSource参数Two")
    @ParameterizedTest
    @MethodSource("personProvider")
    void testWithPersonMethodSourceTwo(Person person) {
        System.out.println(person.getName());
        Assertions.assertNotNull(person.getName());
    }
    static Stream<Person> personProvider() {
        return Stream.of(JMockData.mock(Person.class), JMockData.mock(Person.class));
    }


    /**
     * 如果测试方法声明了多个参数,则需要返回一个Arguments实例的集合或Stream,
     * 如下面代码所示。请注意, Arguments.of(Object ...)是Arguments接口中定义的静态工厂方法。
     */
    @DisplayName("测试MethodSource参数Three")
    @ParameterizedTest
    @MethodSource("stringIntAndListProvider")
    void testWithMultiArgMethodSourceThree(String str, int num, List<String> list) {
        assertEquals(2, str.length());
        Assertions.assertTrue(num >= 1 && num <= 2);
        assertEquals(3, list.size());
    }
    static Stream<Arguments> stringIntAndListProvider() {
        return Stream.of(Arguments.of("bj", 1, Arrays.asList("a", "b","c")),
                Arguments.of("lw", 2, Arrays.asList("x", "y","z")));
    }


    /**
     * @ CsvSource使用单引号'作为引用字符。请参考上述示例和下表中的'bj,lw'值。一个空的引用值''表示一个空 的String;
     * 而一个完全空的值被当成一个null引用。如果null引用的目标类型是基本类型,则会抛出一个ArgumentConversionException。
     */
    @DisplayName("测试CsvSource参数One")
    @ParameterizedTest
    @CsvSource({"bj, 1", "lw, 2", "'bj,lw', 3"})
    void testWithCsvSourceOne(String first, int second) {
        Assertions.assertNotNull(first);
        assertNotEquals(0, second);
    }

    @DisplayName("测试CsvSource参数Two")
    @ParameterizedTest
    @CsvSource({"bj,1,11", "lw,2,12", "'bj,lw',3,13"})
    void testWithCsvSourceTwo(String first, int second,int three) {
        Assertions.assertNotNull(first);
        assertNotEquals(0, second);
        Assertions.assertTrue(three > 10);
    }

    /**
     * @ CsvFileSource允许你使用类路径中的CSV文件。CSV文件中的每一行都会触发参数化测试的一次调用。
     * 与@CsvSource中使用的语法相反,@CsvFileSource使用双引号"作为引号字符,请参考上面例子中的"bj,
     * lw"值,一个空的带引号的值""表示一个空String,一个完全为空的值被当成null引用,如果null引用的目标
     * 类型是基本类型,则会抛出一个ArgumentConversionException。
     */
    @DisplayName("测试CsvFileSource参数")
    @ParameterizedTest
    @CsvFileSource(resources = "/two-column.csv")
    void testWithCsvFileSource(String first, int second) {
        Assertions.assertNotNull(first);
        assertNotEquals(0, second);
    }


    /**
     * @ ArgumentsSource 可以用来指定一个自定义且能够复用的ArgumentsProvider。
     */
    @DisplayName("测试ArgumentsSource参数")
    @ParameterizedTest
    @ArgumentsSource(MyArgumentsProvider.class)
    void testWithArgumentsSource(String argument) {
        Assertions.assertNotNull(argument);
    }
    static class MyArgumentsProvider implements ArgumentsProvider {
        @Override
        public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
            return Stream.of("lw", "bj").map(Arguments::of);
        }
    }
}

动态测试

@DisplayName("动态测试")
public class DynamicTestsDemo {

    @TestFactory
    Collection<DynamicTest> dynamicTestsFromCollection() {
        return Arrays.asList(dynamicTest("1st dynamic test", Assertions::fail),
                dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2)));
    }

    @TestFactory
    Iterable<DynamicTest> dynamicTestsFromIterable() {
        return Arrays.asList(dynamicTest("3rd dynamic test", () -> assertTrue(true)),
                dynamicTest("4th dynamic test", () -> assertEquals(4, 2 * 2)));
    }

    @TestFactory
    Iterator<DynamicTest> dynamicTestsFromIterator() {
        return Arrays.asList(dynamicTest("5th dynamic test", () -> assertTrue(true)),
                dynamicTest("6th dynamic test", () -> assertEquals(4, 2 * 2))).iterator();
    }

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromStream() {
        return Stream.of("A", "B", "C").map(str -> dynamicTest("test" + str, () -> assertTrue(true)));
    }

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromIntStream() {
        return IntStream.iterate(0, n -> n + 2).limit(10)
                .mapToObj(n -> dynamicTest("test" + n, () -> assertEquals(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()))
                                        )
                                )
                        )
                ));
    }
}

测试发现与注册

public static void main(String[] args) {
        LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
                .selectors(
                        selectPackage("top.qrainly.share.test.junit.junit5.example.base")
                        , selectClass(SimpleTest.class), selectClass(DITest.class), selectClass(TimeOutTest.class)
                )
                .filters(
                        includeClassNamePatterns(".*Test")
                        ,excludeClassNamePatterns(".TimeOut*")
                )
                .build();

        Launcher launcher = LauncherFactory.create();

        TestPlan testPlan = launcher.discover(request);
        System.out.println(JSONObject.toJSONString(testPlan));

        // 注册执行
        TestExecutionListener listener = new SummaryGeneratingListener();
        launcher.registerTestExecutionListeners(listener);

        launcher.execute(request);
    }

持续更新中…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qrainly

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值