JUnit 测试在 H2 的 RUNSCRIPT 完成之前开始



我们有一个使用Spring和MAVEN的Java项目。在这个项目中,我们使用内存中的 H2 数据库在我们的 DAO/存储库层上执行多个测试。

运行一些测试后,但并非总是如此,我们收到以下错误:

org.h2.jdbc.JdbcSQLException: Table "WEATHER" not found; SQL statement:

如果您单独执行 JUnit 测试,它永远不会失败。没有关于何时出现错误的模式。

我怀疑下面关于 URL 连接的 RUNSCRIPT 语句没有完成,当单元测试开始时,即执行是异步执行的。

以下是连接声明:

String jdbcUrl = "jdbc:h2:mem:WeatherAPI;MODE=MySQL;DB_CLOSE_ON_EXIT=TRUE;TRACE_LEVEL_SYSTEM_OUT=1;INIT=runscript from 'src/test/resources/sql/weatherapi.sql'"

这个想法是数据库将在每次测试时重置。

下面是用于获取数据源对象的代码片段:

private static java.sql.DataSource ds = null;
public static DataSource getDs() {
    if(this.ds==null) {
        try {
            this.ds = manualCreateDataSource();
        } catch (Exception e) {
            logger.error("Could not initialize Datasource", e);
            throw new RuntimeException("Could not initialize Datasource");
        }
    }
    return this.ds;
}
public static DataSource manualCreateDataSource() {
    String driverClass = "org.h2.jdbcx.JdbcDataSource";
    String jdbcUrl = "jdbc:h2:mem:WeatherAPI;MODE=MySQL;DB_CLOSE_ON_EXIT=TRUE;TRACE_LEVEL_SYSTEM_OUT=1;INIT=runscript from 'src/test/resources/sql/weatherapi.sql'";
    int maxPoolSize = 20;
    int minPoolSize = 5;
    int unreturnedConnectionTimeout = 10;
    int idleConnectionTestPeriod = 200;
    int maxIdleTime = 1000;
    int maxStatementsPerConnection = 5;
    ComboPooledDataSource ds = new ComboPooledDataSource();
    ds.setJdbcUrl(jdbcUrl);
    ds.setMaxPoolSize(maxPoolSize);
    ds.setMinPoolSize(minPoolSize);
    ds.setInitialPoolSize(minPoolSize);
    ds.setUnreturnedConnectionTimeout(unreturnedConnectionTimeout);
    ds.setIdleConnectionTestPeriod(idleConnectionTestPeriod);
    ds.setMaxIdleTime(maxIdleTime);
    ds.setMaxStatementsPerConnection(maxStatementsPerConnection);
    try {
        ds.setDriverClass(driverClass);
    } catch (PropertyVetoException e) {
        logger.error("error setting driver class", e);
    }
    return ds;
}

这里有一个 weatherapi .sql 脚本的片段:

CREATE SCHEMA IF NOT EXISTS `WeatherAPI`;
USE `WeatherAPI`;
DROP TABLE IF EXISTS `Weather`;
CREATE TABLE IF NOT EXISTS `Weather` (
    id int(11) NOT NULL AUTO_INCREMENT,
    location char(3) NOT NULL,
    period varchar(8) NOT NULL,
    duration char DEFAULT NULL,
    payload TEXT,
    created timestamp NULL DEFAULT NULL,
    lastmodified timestamp NULL DEFAULT NULL,
    version int(11) NOT NULL,  PRIMARY KEY (id)
);

我怀疑这是一个竞争条件。根据文档,将为连接到数据库的每个客户端执行脚本。由于您总是在重新创建Weather表之前将其删除,因此当测试A正在运行并且第二个客户端B连接到数据库时,该表可能会直接放在A的鼻子下。 B可能是并行运行的另一个测试,也可能是同一测试中的第二个线程。

如果是这种情况,您可以尝试在 manualCreateDataSource() 方法中使用 RunScript 工具,而不是 JDBC 连接 URL 中的 INIT 参数:

String jdbcUrl = "jdbc:h2:mem:WeatherAPI;MODE=MySQL;DB_CLOSE_ON_EXIT=TRUE;TRACE_LEVEL_SYSTEM_OUT=1;"
RunScript.execute(jdbcUrl, sa, "", "src/test/resources/sql/weatherapi.sql", null, false);

此外,您需要通过向其添加synchronized来使getDs()线程安全,或者更好的是,通过静态初始化实例变量ds

private static java.sql.DataSource ds = manualCreateDataSource();
public static DataSource getDs() {
    return ds;
}

hzpz 的答案实际上帮助我了解发生了什么:竞争条件。我输入的问题没有指定我正在使用 maven(我为此道歉(,我发现 maven surefire 插件正在分叉测试,因此实际上出现了竞争条件。我决定以这种方式关闭分叉并配置maven-surefire插件:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <forkCount>1</forkCount>
        <reuseForks>false</reuseForks>
    </configuration>
</plugin>

关于分叉有几个问题,但没有一个将 H2 的 RUNSCRIPT 与竞争条件联系起来。

这里有关于万无一失的插件的更多详细信息:

Maven 万无一失的插件 - 类加载和分叉

最新更新