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

JUnit test starts before H2´s RUNSCRIPT finishes

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

经过几次测试 运行,但并非总是如此,我们收到以下错误:

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)
);

我怀疑这是一个竞争条件。根据 documentation 脚本为连接到数据库的每个客户端执行。由于您总是在重新创建 Weather table 之前删除它,因此当测试 A 已经 运行 并且第二个客户端 B 连接到时,可能会发生这种情况数据库,table 就在 A 眼皮子底下掉落了。 B 可能是另一个并行运行的测试或同一测试中的第二个线程。

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

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 或更好地通过静态初始化实例变量 ds 来使 getDs() 线程安全:

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>

有几个关于分叉的问题,但是 none 将 H2 的 RUNSCRIPT 与竞争条件相关联。

这里有更多关于 surefire 插件的细节:

Maven Surefire Plugin - Class loading and forking