本文介绍如何在junit测试中避免静态系统属性污染,通过重构 datasource 类和测试基类,确保每次测试前都能独立、可重复地初始化 h2 内存数据库,而非复用 postgres 连接。
本文介绍如何在junit测试中避免静态系统属性污染,通过重构 datasource 类和测试基类,确保每次测试前都能独立、可重复地初始化 h2 内存数据库,而非复用 postgres 连接。
问题根源在于 DataSource 类严重依赖静态字段(config 和 ds)及全局 System.setProperty(),导致 @BeforeEach 无法生效:一旦 ds 被初始化为 PostgreSQL 数据源,后续测试即使修改了系统属性,getDataSource() 仍直接返回已缓存的 HikariDataSource 实例,跳过重新配置逻辑。
要真正实现“每次测试前重置环境”,必须打破静态单例与系统属性的强耦合。以下是推荐的重构方案:
✅ 1. 解耦 DataSource 初始化逻辑(移除静态单例)
将 DataSource 改为非静态、可实例化、可配置的类,支持传入连接参数:
public class DataSource {
private final HikariConfig config;
private final HikariDataSource ds;
// 构造时即完成配置与初始化(无延迟加载副作用)
public DataSource(String jdbcUrl, String username, String password, String liquibaseFile) {
this.config = new HikariConfig();
this.config.setJdbcUrl(jdbcUrl);
this.config.setUsername(username);
this.config.setPassword(password);
this.ds = new HikariDataSource(config);
initDatabase(liquibaseFile);
}
private void initDatabase(String liquibaseFile) {
try (Connection conn = ds.getConnection()) {
DatabaseConnection connection = new JdbcConnection(conn);
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(connection);
Liquibase liquibase = new Liquibase(
liquibaseFile,
new ClassLoaderResourceAccessor(),
database
);
liquibase.update(new Contexts());
} catch (SQLException | LiquibaseException e) {
throw new RuntimeException("Failed to initialize test database", e);
}
}
public HikariDataSource getDataSource() {
return ds;
}
public Connection getConnection() throws SQLException {
return ds.getConnection();
}
}
? 关键改进:
- 消除 static 字段,避免跨测试污染;
- 构造函数强制注入所有依赖参数,明确职责边界;
- initDatabase() 在构造时执行,确保每次新建 DataSource 实例都对应一次完整、隔离的 DB 初始化。
✅ 2. 测试基类改为实例化 + @BeforeEach 驱动
public class Base {
protected DataSource dataSource;
@BeforeEach
void init() {
// 每次测试前创建全新 DataSource(H2 内存库)
String jdbcUrl = "jdbc:h2:mem:test_" + UUID.randomUUID() + ";DB_CLOSE_DELAY=-1";
dataSource = new DataSource(
jdbcUrl,
"sa",
"",
"liquibase_test.xml"
);
}
// 可选:提供便捷获取连接的方法
protected Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
⚠️ 注意事项:
- DB_CLOSE_DELAY=-1 确保 H2 内存库在 JVM 退出前不自动关闭,适配多测试场景;
- 不再使用 System.setProperty(),彻底规避全局状态干扰;
- 所有 DAO 测试继承 Base 后,可直接使用 dataSource 或 getConnection(),行为完全隔离。
✅ 3. DAO 测试示例(验证隔离性)
class UserDaoTest extends Base {
private UserDao userDao;
@BeforeEach
void setUp() {
this.userDao = new UserDao(dataSource.getDataSource()); // 使用当前测试专属数据源
}
@Test
void shouldInsertUserAndFindById() throws SQLException {
try (Connection conn = getConnection()) {
// 插入测试数据
userDao.insert(conn, new User("alice", "alice@example.com"));
// 查询验证
Optional<user> found = userDao.findById(conn, 1L);
assertTrue(found.isPresent());
assertEquals("alice", found.get().getName());
}
}
}</user>
? 总结
- ❌ 不要依赖 System.setProperty() + 静态单例:它违背测试隔离原则,@BeforeEach 对静态字段无效;
- ✅ 用构造注入替代全局配置:让 DataSource 成为可重复创建、可预测的测试友好的组件;
- ✅ 每个测试拥有专属数据库实例:通过 UUID 生成唯一 H2 数据库名,配合 DB_CLOSE_DELAY,既保证隔离又避免资源泄漏;
- ✅ 生产与测试使用相同数据库类型(如 H2 兼容模式)更可靠:若必须用 PostgreSQL 做集成测试,建议单独运行,而非混入单元测试流程。
该方案不仅解决了当前问题,还显著提升了代码可测试性、可维护性与可扩展性。











