使用TestContainers进行数据库测试

如果您曾经编写过测试数据库交互的代码,例如数据访问对象,那么您很可能遇到了测试中最长期的烦恼之一:为了准确地测试这些交互,需要一个数据库。

为了本文的方便,让我们考虑一个将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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值