第4章 使用ExternalResource管理资源

本章目标:掌握通过继承 ExternalResource 并重写 before() / after() 实现数据库连接的可控分配与自动释放,理解其作为 JUnit TestRule 夹具分层中的资源管理核心模式。

4.1 原理说明

ExternalResource 是 JUnit 4.7 引入的 TestRule 抽象子类,专门管理需要在每个测试前后进行生命周期的外部资源(如文件流、网络连接、数据库链接)。其核心机制是模板方法模式:子类实现 before() 完成资源获取,实现 after() 完成资源释放。JUnit 保证无论测试通过、失败或抛出异常,after() 都会被执行,从而防止资源泄漏。

继承链:
TestRule → ExternalResource → 你的资源管理 Rule(如 DatabaseResource)

4.2 实现步骤

  1. 定义继承 ExternalResource 的类,例如 DatabaseResource。
  2. 在类中保存数据库连接对象(如 Connection)作为字段。
  3. 重写 protected void before() throws Throwable:建立连接,可设置自动提交、初始化数据源。
  4. 重写 protected void after():关闭连接(和 Statement/PreparedStatement 等),置空引用。
  5. 在测试类中通过 @Rule 或 @ClassRule 注入该 Rule 实例。
  6. 在测试方法中通过 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 会正确处理
ExternalResource 生命周期图 ExternalResource 生命周期与资源管理流程 JUnit 执行 @Rule before() 获取连接 成功? 是 测试方法执行 after() 关闭连接 否 跳过测试 测试结果(成功/失败) 关键保证 • after() 在测试结束后必定执行 • before() 失败则跳过测试但不调用after • 资源获取应幂等、可重入

4.7 优化技巧

  • 使用 @ClassRule 复用连接池:将 DatabaseResource 声明为 @ClassRule,所有测试方法共享同一个连接池,显著减少连接创建开销。
  • 结合 @Before / @After 做事务隔离:在 Rule 的 before() 中开始事务,在 after() 中回滚,保证每个测试不受其他测试影响。
  • 泛型化 ExternalResource:创建抽象 ResourceRule<T> extends ExternalResource,支持任意资源类型,提高代码复用。
  • 异常边界处理:在 after() 中捕获所有异常并记录,不要吞掉原始测试异常(可配合 MultipleFailureException)。
最佳实践总结:ExternalResource 是 JUnit 框架中最优雅的资源夹具模式,适合管理数据库连接、文件句柄、套接字等有明确打开/关闭语义的资源。结合日志和连接池,能实现测试的可靠、可审计与高性能。