【JUnit实战3_03】第二章:探索 JUnit 的核心功能(二)

《JUnit in Action》全新第3版封面截图

写在前面

再次强调,这一章的重点是快速扫盲,因此知识点相对密集,但并未深入展开讨论。梳理本章知识点时我也只记录核心要点,只在个别实测过程中略作补充。现在 AI 工具如此便捷,对提到的 JUnit 特性如果有疑问,可以快速得到满意的答复。对于需要夯实基础的朋友来说,能逐一消化每个功能特性固然很好;如果条件不允许,至少也要建立印象,以便今后知道往什么方向查阅资料。

文章目录

    • [2.5 @DisplayName 注解](#2.5 @DisplayName 注解)
    • [2.6 @Nested 注解](#2.6 @Nested 注解)
    • [2.7 @Tag 注解](#2.7 @Tag 注解)
    • [2.8 断言方法](#2.8 断言方法)
    • [2.9 新版超时断言](#2.9 新版超时断言)
    • [2.10 需要抛出异常的断言测试](#2.10 需要抛出异常的断言测试)
    • [2.11 假设断言](#2.11 假设断言)

(接上篇)

2.5 @DisplayName 注解

(详见 上一篇


2.6 @Nested 注解

用于测试内部类中的待测试方法。

内部类的经典应用场景是通过 Builder 模式初始化一个类(强烈建议自行手动实现一遍 Customer 实体类,加深印象):

java 复制代码
public class Customer {
    private final Gender gender;
    private final String firstName;
    private final String lastName;

    private final String middleName;
    private final Date becomeCustomer;

    public static class Builder {
        private final Gender gender;
        private final String lastName;
        private final String firstName;

        private String middleName;
        private Date becomeCustomer;
        
        public Builder(Gender gender, String firstName, String lastName) {
            this.gender = gender;
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public Builder withMiddleName(String middleName) {
            this.middleName = middleName;
            return this;
        }

        public Builder withBecomeCustomer(Date becomeCustomer) {
            this.becomeCustomer = becomeCustomer;
            return this;
        }

        public Customer build() {
            return new Customer(this);
        }
    }
    
    private Customer(Builder builder) {
        this.gender = builder.gender;
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.middleName = builder.middleName;
        this.becomeCustomer = builder.becomeCustomer;
    }
    // getters
}

@Nested 注解的用法(L5):

java 复制代码
public class NestedTestsTest {
    private static final String FIRST_NAME = "John";
    private static final String LAST_NAME = "Smith";

    @Nested()
    class BuilderTest {
        private final String MIDDLE_NAME = "Michael";

        @Test
        void customerBuilder() throws ParseException {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MM-dd-yyyy");
            Date customerDate = simpleDateFormat.parse("04-21-2019");

            Customer customer = new Customer.Builder(Gender.MALE, FIRST_NAME, LAST_NAME)
                    .withMiddleName(MIDDLE_NAME)
                    .withBecomeCustomer(customerDate)
                    .build();
            
            assertAll(() -> {
                assertEquals(Gender.MALE, customer.getGender());
                assertEquals(FIRST_NAME, customer.getFirstName());
                assertEquals(LAST_NAME, customer.getLastName());
                assertEquals(MIDDLE_NAME, customer.getMiddleName());
                assertEquals(customerDate, customer.getBecomeCustomer());
            });
        }
    }
}

这部分最吸引眼球的是 Builder 构建模式的手动实现,以及全新的断言方法 assertAll()JDK 8Lambda 表达式的结合。根据 assertAll 的签名,最后的断言逻辑还可以改写为如下模式,并且都能起到"执行所有断言、但不因某一个失败而中断后续断言的判定"的目的:

java 复制代码
assertAll(
    () -> assertEquals(Gender.MALE, customer.getGender()),
    () -> assertEquals(FIRST_NAME, customer.getFirstName()),
    () -> assertEquals(LAST_NAME, customer.getLastName()),
    () -> assertEquals(MIDDLE_NAME, customer.getMiddleName()),
    () -> assertEquals(customerDate, customer.getBecomeCustomer())
);

本地 IDEA 的实测效果如下(起到了很好的分组效果):

2.7 @Tag 注解

该注解是 JUnit 4@Category 的升级版,可通过 IDEpom.xml 配置,实现指定类别的测试类或测试方法的分组运行。

pom.xml 配置(推荐做法):

xml 复制代码
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.22.2</version>
            <configuration>
                <groups>individual</groups>
                <excludedGroups>repository</excludedGroups>
            </configuration>
        </plugin>
</build>

IDEA 配置:

原书截图界面和实测版本相差较大,新版 IDEA 已通过运行 配置文件 来完成相关设置。根据实测情况,Tags 标签可用 |& 等符号实现多个标签的组合运行(分别表示 )。特别地,对于 的情况还有两种写法:

java 复制代码
// version 1
@Tag("individual")
@Tag("repository")
public class CustomerTest {
    // snip
}

// version 2
@Tags({@Tag("individual"), @Tag("repository")})
public class CustomerTest {
    // snip
}

其中第二种的可读性更好。启用 @Tag 注解后,执行命令 mvn test 将只对指定了标签、且明确设置参与测试的单元测试用例才会最终执行。由于无法保证运行测试的人都使用 IDEA,因此更推荐使用 pom.xml 来配置 Tag 标签。

2.8 断言方法

新版 JUnit 5 提供了大量的断言方法,并支持 Java 8 的函数式声明提高测试性能。常见的几种有:

断言方法 功能
assertAll 断言所有提供的可执行对象都不会抛出异常,参数类型为 org.junit.jupiter.api.function.Executable 型对象或对象数组。
assertArrayEquals 断言预期数组与实际数组相等。
assertEquals 断言期望值与实际值相等。
assertX(..., String message) 当断言失败时,向测试框架提供指定消息的断言。
assertX(..., Supplier<String> msgSupplier) 当断言失败时,向测试框架提供指定消息的断言。报错后的消息提示会通过 msgSupplier 延迟获取。

此外,JUnit 4 中的 assertThat 断言在新版中被移除,该断言由 JUnit 第三方辅助框架 Hamcrest 重新实现,更加灵活且符合 Java 8 特性。

关于 Hamcrest 框架

该框架是辅助编写 JUnit 测试用例的第三方工具框架,内置了大量可读性极强的断言方法和辅助工具(各种 matcher 匹配器)。其名称 Hamcrest 就是 matchers 各字母变位后的组合单词,以突出其灵活实用的断言特性。

2.9 新版超时断言

对于超时场景下的断言测试,JUnit 5 提供了两种超时机制:

  • 超时后立即停止测试,不等待可执行的目标代码最终完成(使用 assertTimeout 断言);
  • 超时后继续执行测试,直到可执行的目标代码最终完成(使用 assertTimeoutPreemptively 断言);
java 复制代码
class AssertTimeoutTest {
    private SUT systemUnderTest = new SUT("Our system under test");

    @Test
    @DisplayName("A job is executed within a timeout")
    void testTimeout() throws InterruptedException {
        systemUnderTest.addJob(new Job("Job 1"));
        assertTimeout(ofMillis(500), () -> systemUnderTest.run(200));
    }

    @Test
    @DisplayName("A job is executed preemptively within a timeout")
    void testTimeoutPreemptively() throws InterruptedException {
        systemUnderTest.addJob(new Job("Job 1"));
        assertTimeoutPreemptively(ofMillis(500), () -> systemUnderTest.run(200));
    }
}

assertTimeout() 超时后的报错信息的句式为:execution exceeded timeout of 100 ms by 193 ms.

assertTimeoutPreemptively() 超时后的报错信息的句式为:execution timed out after 100 ms.

2.10 需要抛出异常的断言测试

JUnit 5 还对需要抛异常的应用场景提供了便捷的断言方法。既可以直接书写 assertThrows() 断言,也可以通过该断言返回的 Throwable 对象作进一步断言,例如断言异常原因是否为指定的内容等。

实测代码如下:

java 复制代码
class AssertThrowsTest {
    private SUT systemUnderTest = new SUT("Our system under test");

    @Test
    @DisplayName("An exception is expected")
    void testExpectedException() {
        assertThrows(NoJobException.class, systemUnderTest::run);
    }

    @Test
    @DisplayName("An exception is caught")
    void testCatchException() {
        Throwable throwable = assertThrows(NoJobException.class, () -> systemUnderTest.run(1000));
        assertEquals("No jobs on the execution list!", throwable.getMessage());
    }
}

2.11 假设断言

应用场景:满足某种前提条件后,方可执行后续的断言测试;否则直接跳过该断言的执行。

示例代码:

java 复制代码
class AssumptionsTest {
    private static String EXPECTED_JAVA_VERSION = "1.8";
    private TestsEnvironment environment = new TestsEnvironment(
            new JavaSpecification(System.getProperty("java.vm.specification.version")),
            new OperationSystem(System.getProperty("os.name"), System.getProperty("os.arch"))
    );

    private SUT systemUnderTest = new SUT();

    @BeforeEach
    void setUp() {
        assumeTrue(environment.isWindows());
    }

    @Test
    void testNoJobToRun() {
        assumingThat(
                () -> environment.getJavaVersion().equals(EXPECTED_JAVA_VERSION),
                () -> assertFalse(systemUnderTest.hasJobToRun()));
    }

    @Test
    void testJobToRun() {
        assumeTrue(environment.isAmd64Architecture());
        systemUnderTest.run(new Job());
        assertTrue(systemUnderTest.hasJobToRun());
    }
}

其中,L12L18L24 均为假设断言,如果该行假设不成立,则后续断言均不会执行。

(未完待续)

相关推荐
谢尔登5 小时前
【Nest】单元测试
单元测试·log4j
我的xiaodoujiao13 小时前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 19--测试框架Pytest基础 3--前后置操作应用
python·学习·测试工具·pytest
程序员三藏1 天前
Jmeter接口测试与压力测试
自动化测试·软件测试·python·测试工具·jmeter·接口测试·压力测试
桃子不淘气1 天前
2:测试平台之DB构建
测试工具
测试老哥1 天前
Postman环境变量设置全攻略
自动化测试·软件测试·python·测试工具·职场和发展·接口测试·postman
胜天半月子1 天前
接口测试 | 使用Postman实际场景化测试
测试工具·接口测试·postman
〆WangBenYan゜1 天前
postman 调用接口设置全局变量
测试工具·lua·postman