第4章 使用ExternalResource管理资源
本章目标:掌握通过继承
ExternalResource 并重写 before() / after() 实现数据库连接的可控分配与自动释放,理解其作为 JUnit TestRule 夹具分层中的资源管理核心模式。
4.1 原理说明
ExternalResource 是 JUnit 4.7 引入的 TestRule 抽象子类,专门管理需要在每个测试前后进行生命周期的外部资源(如文件流、网络连接、数据库链接)。其核心机制是模板方法模式:子类实现 before() 完成资源获取,实现 after() 完成资源释放。JUnit 保证无论测试通过、失败或抛出异常,after() 都会被执行,从而防止资源泄漏。
继承链:
TestRule → ExternalResource → 你的资源管理 Rule(如 DatabaseResource)
4.2 实现步骤
- 定义继承
ExternalResource的类,例如DatabaseResource。 - 在类中保存数据库连接对象(如
Connection)作为字段。 - 重写
protected void before() throws Throwable:建立连接,可设置自动提交、初始化数据源。 - 重写
protected void after():关闭连接(和 Statement/PreparedStatement 等),置空引用。 - 在测试类中通过
@Rule或@ClassRule注入该 Rule 实例。 - 在测试方法中通过 Rule 提供的公共 getter 获取连接,执行 SQL。
4.3 代码示例
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExternalResource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class DatabaseResource extends ExternalResource {
private Connection connection;
private final String url = "jdbc:h2:mem:test";
private final String user = "sa";
private final String password = "";
@Override
protected void before() throws Throwable {
connection = DriverManager.getConnection(url, user, password);
System.out.println("[DatabaseResource] Connection opened: " + connection);
// 可选:初始化表结构
try (Statement stmt = connection.createStatement()) {
stmt.execute("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, name VARCHAR(50))");
}
}
@Override
protected void after() {
if (connection != null) {
try {
connection.close();
System.out.println("[DatabaseResource] Connection closed.");
} catch (Exception e) {
throw new RuntimeException("Failed to close connection", e);
}
}
}
public Connection getConnection() {
return connection;
}
}
public class UserDaoTest {
@Rule
public DatabaseResource dbResource = new DatabaseResource();
@Test
public void shouldInsertUser() throws Exception {
Connection conn = dbResource.getConnection();
try (Statement stmt = conn.createStatement()) {
stmt.execute("INSERT INTO users (id, name) VALUES (1, 'Alice')");
ResultSet rs = stmt.executeQuery("SELECT name FROM users WHERE id=1");
rs.next();
System.out.println("User name: " + rs.getString("name"));
}
}
}
4.4 配置细节
| 配置项 | 推荐方式 | 说明 |
|---|---|---|
| 数据库 URL / 凭据 | 构造函数参数 或 系统属性 | 避免硬编码在 Rule 内,可通过构造器注入提高可复用性 |
| 连接池 | Rule 内使用轻量连接池(如 HikariCP) | 提升多测试方法效率,after() 时关闭整个池 |
| 事务控制 | 在每个测试后回滚(通过注解或 Rule 内重置) | 保持测试隔离,before() 设置 autoCommit=false |
| 日志输出 | SLF4J + 测试日志配置 | 便于观察资源生命周期,定位泄漏 |
4.5 调试方法
- 打印生命周期事件:在
before()和after()添加日志(如上例 System.out),运行测试观察控制台输出顺序。 - 启用数据库连接监控:在 H2 等内存数据库中使用
TRACE_LEVEL_SYSTEM_OUT=2查看连接打开/关闭细节。 - 断点调试:在
after()内设置条件断点,某些测试失败时检查连接是否仍处于开放状态。 - 模拟超时异常:在
before()中故意抛出异常,验证after()不会被错误跳过(JUnit 保证不会执行 after 如果 before 失败)——但仍需确保资源状态一致性。
特别注意:
ExternalResource 的 after() 只会在 before() 成功完成后才被调用。如果 before() 抛出异常,不释放任何资源(因为未获取到)。若 before() 获取了部分资源后失败,需要显式回滚,建议在 before() 内使用 try-finally 或委托给另一个资源管理对象。
4.6 常见错误及优化
| 错误模式 | 后果 | 优化方案 |
|---|---|---|
after() 中未关闭 Statement / ResultSet | 游标泄漏,内存浪费 | 在 after() 前关闭所有衍生资源,或使用 try-with-resources 在测试方法内关闭 |
| 每个测试都创建新连接(无连接池) | 速度慢,数据库压力大 | 使用连接池 + @ClassRule 使连接跨方法复用 |
before() 中抛异常后资源未清理 | 资源泄漏 | 采用“获取即注册”模式:在 before() 中用 List 跟踪已打开资源,若后续失败则回滚已打开项 |
@Rule 声明为 public 但实例化在 @Before 中 | Rule 生命周期混乱 | 始终在字段声明时初始化 Rule,JUnit 会正确处理 |
4.7 优化技巧
- 使用 @ClassRule 复用连接池:将
DatabaseResource声明为@ClassRule,所有测试方法共享同一个连接池,显著减少连接创建开销。 - 结合 @Before / @After 做事务隔离:在 Rule 的
before()中开始事务,在after()中回滚,保证每个测试不受其他测试影响。 - 泛型化 ExternalResource:创建抽象
ResourceRule<T> extends ExternalResource,支持任意资源类型,提高代码复用。 - 异常边界处理:在
after()中捕获所有异常并记录,不要吞掉原始测试异常(可配合MultipleFailureException)。
最佳实践总结:ExternalResource 是 JUnit 框架中最优雅的资源夹具模式,适合管理数据库连接、文件句柄、套接字等有明确打开/关闭语义的资源。结合日志和连接池,能实现测试的可靠、可审计与高性能。