当前位置: 首页 > news >正文

【JUnit实战3_23】 第十四章:JUnit 5 扩展模型(Extension API)实战(上)

JUnit in Action, Third Edition

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

写在前面
本章从 JUnit 5 新增的 Extension API 模型中摘取了几个常见场景进行重点介绍。根据最新的 JUnit 官方文档,Extension API 模型接口已经扩充到了 16 个之多;但是有了本章的案例,其他接口也很快能触类旁通。为了方便实测,我又补充了不使用 Extension 接口的常规版本作为上篇进行对照;下篇重点梳理提到了几个应用场景。

第四部分:现代框架与 JUnit 5 实战

该板块是全书的核心内容——

  • 第 14 章介绍 JUnit 5 全新的扩展(extension)模型;
  • 第 15 章介绍表现层测试及 HtmlUnitSelenium 工具的使用;
  • 第 16、17 章介绍 SpringSpringBootJUnit 5 的用法;
  • 第 18 章介绍 REST 风格接口应用的测试;
  • 第 19 章介绍数据库应用的测试(JDBCSpringHibernate)。

第十四章:JUnit 5 扩展模型

本章概要

  • JUnit 5 扩展(extension)的创建方法;
  • 利用扩展点实现 JUnit 5 测试用例的方法;
  • 学会基于 JUnit 5 扩展模型生成的测试用例开发应用程序。

The wheel is an extension of the foot, the book is an extension of the eye, clothing an extension of the skin, electric circuitry an extension of the central nervous system.
轮子是脚的延伸,书籍是眼睛的延伸,衣物是皮肤的延伸,电路是中枢神经系统的延伸。

—— Marshall McLuhan

14.1 JUnit 5 扩展模型概述

JUnit 4 通过 RunnersRules 提供扩展接口,JUnit 5 则通过全新的 Extension API 接口。Extension 本身只是一个 标记接口(marker interface,也叫 标签接口(tag interface令牌接口(token interface,其内部不包含任何字段或接口方法,只用于标识其实现类具备某种特定行为,如 SerializableCloneable 接口等。

扩展模型的基本原理:JUnit 5 扩展的具体实现逻辑,可以关联到测试执行过程中的某个特定事件的发生节点,即 扩展点(extension point。当测试声明周期抵挡该节点时,JUnit 引擎就会自动调用这些扩展逻辑。

JUnit 5 提供了以下几类扩展点:

  • 条件测试执行(Conditional test execution):根据某个判定条件决定测试是否应该运行;
  • 生命周期回调(Life-cycle callback):响应测试的生命周期内的特定事件;
  • 参数解析(Parameter resolution:解析测试运行时接收到的参数;
  • 异常处理(Exception handling:在测试遇到特定类型的异常时,定义测试的行为;
  • 测试实例后处理(Test instance postprocessing:在测试实例创建后需要执行的具体逻辑。

JUnit 5 的扩展逻辑常被框架或构建工具内部调用,也可用于应用开发,只是程度有限。

14.2 示例一:定制判定条件选择性执行测试

思路:实现 org.junit.jupiter.api.extension.ExecutionCondition 接口,并重写 evaluateExecutionCondition() 方法。

示例从资源目录下的配置文件 context.properties 读取参数 context 的值,并通过验证这个值是否在指定的值域范围内(regularlow)来决定被标注的测试类或方法是否执行:

public class ExecutionContextExtension implements ExecutionCondition {@Overridepublic ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {Properties properties = new Properties();String executionContext = "";try {properties.load(getClass().getClassLoader().getResourceAsStream("context.properties"));executionContext = properties.getProperty("context");if (!"regular".equalsIgnoreCase(executionContext) && !"low".equalsIgnoreCase(executionContext)) {return ConditionEvaluationResult.disabled("Test disabled outside regular and low contexts");}} catch (IOException e) {throw new RuntimeException(e);}return ConditionEvaluationResult.enabled("Test enabled on the " + executionContext + " context");}
}// 用法:
@ExtendWith({ExecutionContextExtension.class})
class PassengerTest {// -- snip --
}

这些被标记的测试类或方法在 IDEA 中按类似 @Disabled 注解的方式跳过测试逻辑。以上一章演示过的 Passenger 乘客实体类为例,快速构建一个包含三个测试方法的测试类 PassengerDemoTest

public class PassengerDemoTest {private static void runTestLogics() {Passenger passenger = new Passenger("123-456-789", "John Smith");assertEquals("Passenger John Smith with identifier: 123-456-789", passenger.toString());}@Test@ExtendWith(ExecutionContextExtension.class)@DisplayName("marked with conditional extension (1)")void testPassenger1() {runTestLogics();}@Test@ExtendWith(ExecutionContextExtension.class)@DisplayName("marked with conditional extension (2)")void testPassenger2() {runTestLogics();}@Test@DisplayName("no conditional extension")void testPassenger3() {runTestLogics();}
}

运行该测试类,IDEA 将跳过被标注的测试方法(效果如同添加了 @Disabled 注解):

Fig14.2

此外,IDEA 还支持通过 虚拟机参数 来禁用这些判定条件,参数写法为:

-Djunit.jupiter.conditions.deactivate=*

实测 IDEA 最新版中的设置界面(IntelliJ IDEA 2025.2.4 (Ultimate Edition)):

Fig14.1

实测效果:

Fig14.3

14.3 示例二:实体类持久化到数据库的 CRUD 操作测试

这个例子是全章的重点,也很有现实意义。演示的业务需求,是要将乘客实体类 Passenger 存入某个数据库,然后用 JUnit 5 创建单元测试。

14.3.1 实现数据持久化逻辑

乘客实体类 Passenger 的具体逻辑不变:

public class Passenger {private String identifier;private String name;public Passenger(String identifier, String name) {this.identifier = identifier;this.name = name;}public String getIdentifier() {return identifier;}public String getName() {return name;}@Overridepublic String toString() {return "Passenger " + getName() + " with identifier: " + getIdentifier();}
}

为了方便演示,书中使用的工具链为:H2 + JDBC + JUnit 5 扩展。

为此,需要在 pom.xml 中引入内存数据库 H2 的依赖(之前的版本 1.4.199 太旧了,实测时升级到最新的 2.4.240):

<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>2.4.240</version>
</dependency>

然后创建工具类 ConnectionManager,利用传统的 JDBC API 实现连接数据库的基础逻辑:

public class ConnectionManager {private static Connection connection;public static Connection getConnection() {return connection;}public static Connection openConnection() {try {Class.forName("org.h2.Driver"); // this is driver for H2connection = DriverManager.getConnection("jdbc:h2:~/passenger","sa", // login"" // password);return connection;} catch (ClassNotFoundException | SQLException e) {throw new RuntimeException(e);}}public static void closeConnection() {if (null != connection) {try {connection.close();} catch (SQLException e) {throw new RuntimeException(e);}}}
}

只创建连接还不够,还得提前建好一张乘客表 PASSENGERS 用于增删改查操作;测试结束后再删除该表(也可以保留,根据具体需求确定),相关逻辑放到另一个工具类 TablesManager

public class TablesManager {public static void createTable(Connection connection) {String sql = "CREATE TABLE IF NOT EXISTS PASSENGERS (ID VARCHAR(50), NAME VARCHAR(50));";executeStatement(connection, sql);}public static void dropTable(Connection connection) {String sql = "DROP TABLE IF EXISTS PASSENGERS;";executeStatement(connection, sql);}private static void executeStatement(Connection connection, String sql) {try (PreparedStatement statement = connection.prepareStatement(sql)) {statement.executeUpdate();} catch (SQLException e) {throw new RuntimeException(e);}}
}

然后是数据访问层逻辑,包含一个 DAO 接口和对应实现。接口层 PassengerDao 定义了 CRUD 四个方法:

// 接口层
public interface PassengerDao {void insert(Passenger passenger);void update(String id, String name);void delete(Passenger passenger);Passenger getById(String id);
}

然后实现该接口,并通过构造函数传参的方式注入数据库连接:

public class PassengerDaoImpl implements PassengerDao {private Connection connection;public PassengerDaoImpl(Connection connection) {this.connection = connection;}@Overridepublic void insert(Passenger passenger){String sql = "INSERT INTO PASSENGERS (ID, NAME) VALUES (?, ?)";try (PreparedStatement statement = connection.prepareStatement(sql)) {statement.setString(1, passenger.getIdentifier());statement.setString(2, passenger.getName());statement.executeUpdate();} catch (SQLException e) {throw new RuntimeException(e);}}@Overridepublic void update(String id, String name) {String sql = "UPDATE PASSENGERS SET NAME = ? WHERE ID = ?";try (PreparedStatement statement = connection.prepareStatement(sql)) {statement.setString(1, name);statement.setString(2, id);statement.executeUpdate();} catch (SQLException e) {throw new RuntimeException(e);}}@Overridepublic void delete(Passenger passenger) {String sql = "DELETE FROM PASSENGERS WHERE ID = ?";try (PreparedStatement statement = connection.prepareStatement(sql)) {statement.setString(1, passenger.getIdentifier());statement.executeUpdate();} catch (SQLException e) {throw new RuntimeException(e);}}@Overridepublic Passenger getById(String id) {String sql = "SELECT * FROM PASSENGERS WHERE ID = ?";Passenger passenger = null;try (PreparedStatement statement = connection.prepareStatement(sql)) {statement.setString(1, id);ResultSet resultSet = statement.executeQuery();if (resultSet.next()) {passenger = new Passenger(resultSet.getString(1), resultSet.getString(2));}} catch (SQLException e) {throw new RuntimeException(e);}return passenger;}
}

这样功能模块就实现好了,下一步该编写测试用例了。

14.3.2 不用 Extension API 的单元测试写法

按照正常逻辑,测试类中也要注入一个 PassengerDao 的依赖,然后在每个测试方法内依次执行下列步骤:

  1. 连接数据库、数据表:交给添加了 @BeforeAll 注解的生命周期方法;
  2. 初始化数据库:交给添加了 @BeforeEach 注解的生命周期方法;
  3. 执行 CRUD 测试逻辑;测试方法的核心逻辑;
  4. 断言 CRUD 执行结果:测试方法的核心逻辑;
  5. 还原数据库操作:交给添加了 @AfterEach 注解的生命周期方法;
  6. 清空数据表、关闭数据库连接:交给添加了 @AfterAll 注解的生命周期方法。

上述步骤除了 34 外,其余都是配合核心逻辑的辅助工作,如果不演示 Extension API 的用法,测试用例应该写成这样:

public class PassengerDiyTest {private static Connection connection;private Savepoint savepoint;private PassengerDao passengerDao;@BeforeAllstatic void setUp() {connection = ConnectionManager.openConnection();TablesManager.dropTable(connection);TablesManager.createTable(connection);}@AfterAllstatic void tearDown() {TablesManager.dropTable(connection);if (null != connection) {try {connection.close();} catch (SQLException e) {throw new RuntimeException(e);}}}@BeforeEachvoid setUpEach() throws SQLException {passengerDao = new PassengerDaoImpl(connection);connection.setAutoCommit(false);savepoint = connection.setSavepoint("savepoint");}@AfterEachvoid tearDownEach() throws SQLException {connection.rollback(savepoint);}@Testvoid testPassenger() {Passenger passenger = new Passenger("123-456-789", "John Smith");assertEquals("Passenger John Smith with identifier: 123-456-789", passenger.toString());}@Testvoid testInsertPassenger() {Passenger passenger = new Passenger("123-456-789", "John Smith");passengerDao.insert(passenger);assertEquals("John Smith", passengerDao.getById("123-456-789").getName());}@Testvoid testUpdatePassenger() {Passenger passenger = new Passenger("123-456-789", "John Smith");passengerDao.insert(passenger);passengerDao.update("123-456-789", "Michael Smith");assertEquals("Michael Smith", passengerDao.getById("123-456-789").getName());}@Testvoid testDeletePassenger() {Passenger passenger = new Passenger("123-456-789", "John Smith");passengerDao.insert(passenger);passengerDao.delete(passenger);assertNull(passengerDao.getById("123-456-789"));}
}

实测效果也是没问题的:

Fig14.4


(上篇完)

http://www.dtcms.com/a/561039.html

相关文章:

  • python_study--week3
  • 【Excalidraw】简洁好看的超轻量级画图白板
  • 手写Autosar架构的CAN通讯协议栈2(CanIf模块详解-上)
  • 【Agentic RL 专题】三、深入浅出强化学习算法 TRPO 和PPO
  • 中国最好的建站公司毕业设计模板
  • 《算法通关指南:数据结构和算法篇 --- 栈相关算法题》--- 1. 【模板】栈,2.有效的括号
  • 高效管理搜索历史:Vue持久化实践
  • html网站架设目录和文章wordpress
  • Rust 编程语言基础知识全面介绍
  • 洛龙区网站制作建设费用做网站一般用什么语言
  • 计算机网络---基础诊断ping
  • 13.2.2.Nginx
  • java后端学习经验分享(大三进大厂版)
  • 好用的镜像源
  • 做网站的经验有什么好的加盟店项目
  • linux-shell-基础与变量和运算符-1
  • 论文解读:Sleeping with One Eye Open: Fast, Sustainable Storage with Sandman
  • 手机客户端网站建设腾讯云服务器免费领取
  • Gorm(十三)主从表的判断
  • 从零开始的云原生之旅(十):HPA 完全指南:从原理到实践
  • 注册网站费属于什么费用模板公司
  • MYSQL-多种方法安装部署
  • 做网站要学哪些代码上海资本公司排名
  • 认识多线程:单例模式
  • 深入解析 HarmonyOS 中 NavDestination 导航目标页的生命周期
  • 3、webgl 基本概念 + 绘制线段 + 绘制三角形
  • 【LeetCode热题100(58/100)】单词搜索
  • 旅行社网站模版网页设计六安模板
  • 求解器驱动智能决策新纪元
  • 简单网站制作成品广东省广州市佛山市