如果您曾经编写过测试数据库交互的代码,例如数据访问对象,那么您很可能遇到了测试中最长期的烦恼之一:为了准确地测试这些交互,需要一个数据库。
为了本文的方便,让我们考虑一个将PostgreSQL用作其环境的一部分的应用程序,因为这是示例所使用的。 同样,尽管H2被广泛提及,但这绝不是要贬低它的意思-在正确的位置使用它是一个很好的工具。
问题
已经提出了解决该问题的各种方法,但是似乎总是存在一些缺点。
一种测试方法是使用内存数据库,例如H2。
优点:
- 数据库在虚拟机本地
- 数据库生命周期由构建过程管理
- 初始状态由构建过程或测试管理
缺点:
- 您没有准确地对环境建模
- 并非支持生产数据库的所有功能
- 不同的数据类型意味着不同的列定义
- 涉及相同表的多个测试不能并行运行而不会产生冲突
如果您认为这些约束是不可接受的,则可以考虑保留一个众所周知的正在运行的PostgreSQL数据库实例用于测试。
优点:
- 与生产数据库100%兼容
缺点:
- 不保证初始数据状态
- 同一版本中涉及相同表的多个测试不能并行运行而不会产生冲突
- 并行构建可能导致结果不一致
- 运行本地测试的开发人员可能会破坏持续的集成构建
这种方法的进一步改进是使每个开发人员都拥有自己的PostgreSQL数据库实例。
优点:
- 与生产数据库100%兼容
- 开发人员构建不会干扰持续集成构建
缺点:
- 不保证初始数据状态
- 同一版本中涉及相同表的多个测试不能并行运行而不会产生冲突
- 并行构建可能导致结果不一致
- 开发人员必须保持其数据库实例为最新(或必须添加工具来对此进行管理)
使用这些方法中的每一种,我都认为不利之处足以部分或完全抵消优点。
外卖
分解最后三段,我们可以看到以下功能是理想的:
- 数据库应绑定到测试(而不是虚拟机)
- 这意味着现在可以进行测试并行化了
- 数据库生命周期应由内部版本管理
- 数据库应与生产中使用的数据库相同
我最喜欢的新解决方案
使用TestContainers ,我们可以勾选每个功能。 使用JUnit @Rule
,TestContainers将启动每个测试的Docker映像,该映像提供一个寿命与测试一样长的数据库。 由于每个Docker实例都是完全隔离的,因此可以并行运行测试以加快构建速度。
最后一点非常重要,因为如上所述,似乎总是存在一些缺点。 在这种情况下,启动Docker映像及其包含的所有内容的开销将增加您的总体构建时间。 我会(并且确实)认为增加的测试时间几乎不会影响拥有我们所有所需功能的好处。
TestContainers开箱即用支持的每个数据库都有一个特定规则,该规则可用于获取连接到数据库所需的所有详细信息。
public class FooDaoTest {
@Rule
public PostgreSQLContainer postgres = new PostgreSQLContainer();
@Before
public void setUp() {
// populate database
// postgres.getDriverClassName()
// postgres.getJdbcUrl()
// postgres.getUsername()
// postgres.getPassword()
}
}
或者...
根据文档 ,可以通过将JDBC URL更改为包含tc:
来启动新容器,例如jdbc:tc:postgresql://hostname/databasename
。 但是,由于驱动程序中存在这一行 ,因此在我的应用程序中失败了。
if (!url.startsWith("jdbc:postgresql:")) {
轶事
在这里,我花了10分钟时间将应用程序从使用H2切换为使用Dockerized PostgreSQL,这使我的生活变得更加简单。 我们使用jOOQ进行数据库交互,发现自己不得不删除一些非常好的jOOQ功能,因为H2不支持它们。
让我重复一遍。 由于测试环境的限制,我们面临着不断变化的生产代码 。
那是不可能的,而且永远不会是一个可以接受的情况,因此TestContainers的发现既是偶然的也是节省时间的。 太好了,因为它确实提供了我们所需的东西,但是省时? 我刚才说这会增加测试时间,怎么说呢? 很简单–我不需要花时间查看是否有H2模式可以支持我正在使用的功能。 我发现自己写的代码以后必须删除,因为H2不允许这样做; 我可以编写测试和与DB相关的代码,然后就完成了。
哇,您没有提到Play的整个博客文章吗?
不。 根据我刚刚提到的应用程序,这是在Play上使用它的一种简便方法。
首先,创建一个将TestContainer与Play的数据库支持结合在一起的mixin。
package be.objectify.tcexample.db;
import com.google.common.collect.ImmutableMap;
import org.testcontainers.containers.PostgreSQLContainer;
import play.db.Database;
import play.db.Databases;
import play.db.evolutions.Evolutions;
public interface DbTestSupport {
default Database create(final PostgreSQLContainer postgres) throws Exception {
final Database database = Databases.createFrom("default",
postgres.getDriverClassName(),
postgres.getJdbcUrl(),
ImmutableMap.of("username", postgres.getUsername(),
"password", postgres.getPassword()));
Evolutions.applyEvolutions(database);
return database;
}
default void destroy(final Database database) {
Evolutions.cleanupEvolutions(database);
database.shutdown();
}
}
我在这里使用mixin的原因是因为倾向于在接口旁边定义DAO测试-请参阅我的[上一篇文章](http://www.objectify.be/wordpress/2013/06/01/a-good-lazy-way -to-write-tests /)。 如果可以将测试定义为mixins,那就更好了,因为可以将通用数据库设置代码放入一个通用类中,然后可以将其扩展以实现测试mixins,但是JUnit无法识别以这种方式定义的测试。
因此,抽象测试类不知道它具有需要数据库的实现-它仅测试接口的约定。
package be.objectify.tcexample;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public abstract AbstractUserDaoTest {
@Test
public void testFoo() {
assertThat(dao().something()).isEqualTo(whatever);
}
// many, many more tests
public abstract UserDao dao();
}
通过我们特定于数据库的实现作为后盾,我们现在可以确保我们的实现按照合同要求的方式运行。
package be.objectify.tcexample.db;
import be.objectify.tcexample.AbstractUserDaoTest;
import be.objectify.tcexample.UserDao;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.testcontainers.containers.PostgreSQLContainer;
import play.db.Database;
public class JooqUserDaoTest extends AbstractUserDaoTest implements DbTestSupport,
TestData {
@Rule
public PostgreSQLContainer postgres = new PostgreSQLContainer();
private Database database;
@Before
public void setup() throws Exception {
// the database has all evolutions applied
database = create(postgres);
// load some test data
loadTestData(database);
}
@After
public void tearDown() {
destroy(database);
}
@Override
public UserDao dao() {
return new JooqUserDao(database);
}
}
现在,我们的JooqUserDao
实现将针对生产中使用的数据库类型的真实实例运行。
JooqUserDaoTest
使用的TestData
接口只是将某些数据加载到数据库中的另一个mixin。 实现并不是特别重要,因为它很大程度上取决于您自己的要求,但是它看起来可能像这样。
package be.objectify.tcexample.db;
import org.jooq.impl.DSL;
import play.db.Database;
import java.sql.Connection;
import java.sql.Timestamp;
import java.time.Instant;
import static be.objectify.tcexample.db.jooq.generated.Tables.ACCOUNT;
public interface TestData {
default void loadTestData(Database database) {
database.withConnection((Connection conn) -> {
DSL.using(conn)
.insertInto(ACCOUNT,
ACCOUNT.ID,
ACCOUNT.KEY,
ACCOUNT.CREATED_ON)
.values(1,
"test-account-a",
Timestamp.from(Instant.now()))
.execute();
DSL.using(conn)
.insertInto(ACCOUNT,
ACCOUNT.ID,
ACCOUNT.KEY,
ACCOUNT.CREATED_ON)
.values(2,
"test-account-b",
Timestamp.from(Instant.now()))
.execute();
});
}
}
翻译自: https://www.javacodegeeks.com/2017/03/database-testing-testcontainers.html