leven.chen
Published on 2024-02-14 / 78 Visits
0
0

Testcontainers Database Integration Testing

引言

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

如果你还在使用HSQLDB或者H2来测试你的Spring Boot应用程序,而你的应用程序在生产环境中却运行在Oracle、SQL Server、PostgreSQL或MySQL上,那么最好切换到Testcontainers。

内存数据库 VS Docker

像HSQLDB和H2这样的内存关系型数据库系统在2000年初被开发出来,主要有两个原因:

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


然而,在2013年,```Docker``` 发布了,使得在任何宿主操作系统上运行一个可以托管关系型数据库的Docker容器变得非常容易。我们现在可以使用Docker进行集成测试,而不是使用内存数据库。

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

Testcontainers数据库集成测试

Testcontainers数据库集成测试

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

```Testcontainers```是一个开源项目,它提供了一个Java API,让你可以以编程方式管理你的Docker容器,这非常有用。

最好的展示Testcontainers工作原理的方式是将其集成到令人惊叹的Hypersistence Utils项目中。

Hypersistence Utils项目为Oracle、SQL Server、PostgreSQL和MySQL提供了对JSON、ARRAY、Ranges、CurrencyUnit、MonetaryAmount、YearMonth等多种类型的支持。

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

因此,你有几个选择:

  • 你可以在本地安装Oracle XE、SQL Server Express、PostgreSQL和MySQL

  • 你可以使用Docker Compose根据compose配置文件启动所有这些数据库

  • 你可以使用Testcontainers来自动化这个过程

在我的情况下,由于我在进行高性能Java持久性培训和高性能SQL培训,并且正在开发Hypersistence Optimizer,所以我已经在我的机器上安装了所有这些前四大关系型数据库。

但是,其他Hypersistence Utils项目贡献者可能没有安装所有这些数据库,因此对他们来说运行项目集成测试将是一个问题。

而这正是Testcontainers发挥作用的地方!

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

将Testcontainers添加到Hypersistence Utils

在使用Testcontainers之前,Hypersistence Utils已经使用了一个DataSourceProvider抽象,允许测试定义所需的数据库,如下所示。

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

当我们启动时,就会看到如下打印日志:

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

结论


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

虽然你可以使用Docker来管理你的容器,但通过使用Testcontainers以编程方式进行操作要方便得多。在我的情况下,只有当没有可用的本地数据库时,Testcontainers才会启动一个Docker容器。


Comment