如何使用Testcontainers进行数据库集成测试

710 阅读5分钟

简介

在这篇文章中,我们将看到如何使用Testcontainers进行数据库集成测试。

如果你还在使用HSQLDBH2来测试你的Spring Boot应用程序,这些应用程序在生产中运行于Oracle、SQL Server、PostgreSQL或MySQL,那么你最好改用Testcontainers

内存数据库与Docker的对比

内存关系型数据库系统,如HSQLDB和H2是在2000年初开发的,有两个原因。

  1. 因为在当时,安装数据库系统是一个非常繁琐的过程,需要花费大量的时间。
  2. 与需要从磁盘加载或刷新到磁盘的数据库相比,内存数据库的访问速度要快好几个数量级。

然而,在2013年,Docker发布了,使得运行一个Docker容器变得非常容易,该容器可以在任何主机操作系统上托管一个关系数据库。

为了加快测试的数据访问速度,你可以将数据文件夹和交易日志挂载到tmpfs内存Linux存储中。

所以,如今我们有了更好的工具,而这些工具在2013年之前我们是没有的。我们现在可以使用Docker进行集成测试,而不是使用内存数据库。

Docker数据库容器的优势在于,你要针对你在生产中使用的同一个数据库版本进行测试。如果你有SQL特定的查询或存储过程,那么你不能使用H2或HSQLDB来测试它们。你需要一个真正的数据库引擎来运行特定的查询、存储过程或函数。

Testcontainers数据库集成测试

虽然你可以手动或使用docker-maven-plugin自动启动和停止Docker数据库容器,但有一个更好的方法。

Tectcontainers是一个开源项目,它提供了一个Java API,这样你就可以以编程方式管理你的Docker容器,这非常有用,你很快就会看到。

而向你展示Testcontainers如何工作的最好方式是将其整合到令人惊叹的Hibernate Types项目中。

Hibernate Types项目提供了对JSON, ARRAY, Ranges,CurrencyUnit,MonetaryAmount,YearMonth, 以及许多其他Oracle, SQL Server, PostgreSQL, 和MySQL的类型的支持。

为了增加对一个新类型的支持,你必须在所有这些关系数据库系统上运行集成测试。

因此,你有几个选择。

  • 你可以在本地安装Oracle XE、SQL Server Express、PostgreSQL和MySQL
  • 你可以使用Docker Compose来启动所有这些基于compose配置文件的数据库
  • 你可以使用Testcontainers来自动完成这个过程。

在我的例子中,我已经在我的机器上安装了所有这些四大关系型数据库,因为我在高性能Java持久性培训高性能SQL培训中大量使用它们,并开发超持久优化器

但是,其他Hibernate Types项目的参与者可能没有安装所有这些数据库,所以运行项目集成测试对他们来说是个问题。

而这正是Testcontainers来拯救的地方!

你可以设置DataSource 提供者连接到预先定义的数据库URL,如果失败,就按需启动Docker容器。

向Hibernate类型添加Testcontainers

在使用Testcontainer之前,Hibernate Types已经使用了一个DataSourceProvider 抽象,允许测试定义所需的数据库,其外观如下。

Testcontainers Database Integration Testing

DataSourceProvider 接口定义了数据库管理合同。

public interface DataSourceProvider {

    Database database();

    String hibernateDialect();

    DataSource dataSource();

    String url();

    String username();

    String password();
}

AbstractContainerDataSourceProvider 定义了Testcontainers Docker数据库容器如何按需启动。

public abstract class AbstractContainerDataSourceProvider 
        implements DataSourceProvider {

    @Override
    public DataSource dataSource() {
        DataSource dataSource = newDataSource();
        try(Connection connection = dataSource.getConnection()) {
            return dataSource;
        } catch (SQLException e) {
            Database database = database();
            if(database.getContainer() == null) {
                database.initContainer(username(), password());
            }
            return newDataSource();
        }
    }

    @Override
    public String url() {
        JdbcDatabaseContainer container = database().getContainer();
        return container != null ?
            container.getJdbcUrl() :
            defaultJdbcUrl();
    }

    protected abstract String defaultJdbcUrl();

    protected abstract DataSource newDataSource();
}

AbstractContainerDataSourceProvider 允许我们使用一个本地数据库,这个数据库首先被解析。如果没有本地数据库可供连接,则通过调用相关Database 对象的initContainer 方法来实例化一个新的数据库容器,该方法看起来如下。

public enum Database {
    POSTGRESQL {
        @Override
        protected JdbcDatabaseContainer newJdbcDatabaseContainer() {
            return new PostgreSQLContainer(
                "postgres:13.7"
            );
        }
    },
    ORACLE {
        @Override
        protected JdbcDatabaseContainer newJdbcDatabaseContainer() {
            return new OracleContainer(
                "gvenzl/oracle-xe:21.3.0-slim"
            );
        }

        @Override
        protected boolean supportsDatabaseName() {
            return false;
        }
    },
    MYSQL {
        @Override
        protected JdbcDatabaseContainer newJdbcDatabaseContainer() {
            return new MySQLContainer(
                "mysql:8.0"
            );
        }
    },
    SQLSERVER {
        @Override
        protected JdbcDatabaseContainer newJdbcDatabaseContainer() {
            return new MSSQLServerContainer(
                "mcr.microsoft.com/mssql/server:2019-latest"
            );
        }

        @Override
        protected boolean supportsDatabaseName() {
            return false;
        }

        @Override
        protected boolean supportsCredentials() {
            return false;
        }
    };

    private JdbcDatabaseContainer container;

    public JdbcDatabaseContainer getContainer() {
        return container;
    }

    public void initContainer(String username, String password) {
        container = (JdbcDatabaseContainer) newJdbcDatabaseContainer()
            .withEnv(Collections.singletonMap("ACCEPT_EULA", "Y"))
            .withTmpFs(Collections.singletonMap("/testtmpfs", "rw"));
            
        if(supportsDatabaseName()) {
            container.withDatabaseName("high-performance-java-persistence");
        }
        
        if(supportsCredentials()) {
            container.withUsername(username).withPassword(password);
        }
        
        container.start();
    }

    protected JdbcDatabaseContainer newJdbcDatabaseContainer() {
        throw new UnsupportedOperationException(
            String.format(
                "The [%s] database was not configured to use Testcontainers!",
                name()
            )
        );
    }

    protected boolean supportsDatabaseName() {
        return true;
    }

    protected boolean supportsCredentials() {
        return true;
    }
}

特定于数据库的DataSource 实例化逻辑被定义在DataSourceProvider 实现中。

public class MySQLDataSourceProvider 
		extends AbstractContainerDataSourceProvider {

    @Override
    public String hibernateDialect() {
        return "org.hibernate.dialect.MySQL8Dialect";
    }

    @Override
    protected String defaultJdbcUrl() {
        return "jdbc:mysql://localhost/high_performance_java_persistence?useSSL=false";
    }

    protected DataSource newDataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setURL(url());
        dataSource.setUser(username());
        dataSource.setPassword(password());

        return dataSource;
    }

    @Override
    public String username() {
        return "mysql";
    }

    @Override
    public String password() {
        return "admin";
    }

    @Override
    public Database database() {
        return Database.MYSQL;
    }
}

这就是了!

如果我停止我的本地MySQL数据库并运行一个MySQL测试,我可以看到一个MySQL容器是按需启动的。

DEBUG [main]: ?.0] - Trying to create JDBC connection using com.mysql.cj.jdbc.Driver to 
    jdbc:mysql://localhost:57127/high-performance-java-persistence?useSSL=false&allowPublicKeyRetrieval=true 
with properties: {
    user=mysql, 
    password=admin
}
INFO  [main]: ?.0] - Container is started (
    JDBC URL: jdbc:mysql://localhost:57127/high-performance-java-persistence
)
INFO  [main]: ?.0] - Container mysql:8.0 started in PT34.213S

总结

在Hibernate Types项目中添加对Testcontainers的支持是一个非常简单的过程。然而,其优势是巨大的,因为现在任何人都可以运行现有的测试案例,而不需要事先进行数据库配置。

虽然你可以使用Docker来管理你的容器,但使用Testcontainers以编程方式来做要方便得多。在我的案例中,Testcontainers允许我只在没有可用的本地数据库时启动Docker容器。