我使用过很多web应用程序,它们都是由后台复杂程度各异的数据库驱动的。通常,有一个ORM层独立于业务和表示逻辑。这使得单元测试业务逻辑相当简单;事情可以在离散的模块中实现,测试所需的任何数据都可以通过对象模拟来伪造。

但是测试ORM和数据库本身总是充满了问题和妥协。

这些年来,我尝试了一些策略,但没有一个能让我完全满意。

Load a test database with known data. Run tests against the ORM and confirm that the right data comes back. The disadvantage here is that your test DB has to keep up with any schema changes in the application database, and might get out of sync. It also relies on artificial data, and may not expose bugs that occur due to stupid user input. Finally, if the test database is small, it won't reveal inefficiencies like a missing index. (OK, that last one isn't really what unit testing should be used for, but it doesn't hurt.) Load a copy of the production database and test against that. The problem here is that you may have no idea what's in the production DB at any given time; your tests may need to be rewritten if data changes over time.

有些人指出,这两种策略都依赖于特定的数据,单元测试应该只测试功能。为此,我看到了一些建议:

使用模拟数据库服务器,只检查ORM是否在响应给定方法调用时发送了正确的查询。

您在测试数据库驱动的应用程序时使用了哪些策略?对你来说最有效的方法是什么?


实际上,我用了你的第一种方法,并取得了相当大的成功,但我认为用一种稍微不同的方式可以解决你的一些问题:

Keep the entire schema and scripts for creating it in source control so that anyone can create the current database schema after a check out. In addition, keep sample data in data files that get loaded by part of the build process. As you discover data that causes errors, add it to your sample data to check that errors don't re-emerge. Use a continuous integration server to build the database schema, load the sample data, and run tests. This is how we keep our test database in sync (rebuilding it at every test run). Though this requires that the CI server have access and ownership of its own dedicated database instance, I say that having our db schema built 3 times a day has dramatically helped find errors that probably would not have been found till just before delivery (if not later). I can't say that I rebuild the schema before every commit. Does anybody? With this approach you won't have to (well maybe we should, but its not a big deal if someone forgets). For my group, user input is done at the application level (not db) so this is tested via standard unit tests.

正在加载生产数据库副本: 这是我在上一份工作中使用的方法。这是一个巨大的痛苦,因为有几个问题:

副本会比生产版本过时 将对副本的模式进行更改,但不会传播到生产系统。在这一点上,我们有不同的模式。不好玩。

模拟数据库服务器: 我现在的工作也是这样。在每次提交之后,我们对注入了模拟db访问器的应用程序代码执行单元测试。然后我们每天执行三次上面描述的完整的db构建。我绝对推荐这两种方法。

我一直在问这个问题,但我认为没有解决这个问题的灵丹妙药。

我目前所做的是模拟DAO对象,并在内存中保持一个良好的对象集合的表示,这些对象表示可能存在于数据库中的有趣的数据情况。

The main problem I see with that approach is that you're covering only the code that interacts with your DAO layer, but never testing the DAO itself, and in my experience I see that a lot of errors happen on that layer as well. I also keep a few unit tests that run against the database (for the sake of using TDD or quick testing locally), but those tests are never run on my continuous integration server, since we don't keep a database for that purpose and I think tests that run on CI server should be self-contained.

我发现另一种方法非常有趣,但并不总是值得的,因为它有点耗时,那就是在只在单元测试中运行的嵌入式数据库上创建用于生产的相同模式。

尽管毫无疑问,这种方法提高了您的覆盖率,但也有一些缺点,因为您必须尽可能接近ANSI SQL,以使其与当前的DBMS和嵌入式替代品一起工作。

无论您认为哪个项目与您的代码更相关,都有一些项目可以使它更简单,比如DbUnit。

出于以下原因,我总是对内存中的DB (HSQLDB或Derby)运行测试:

它使您考虑在测试DB中保留哪些数据以及为什么要保留这些数据。仅仅将您的生产DB拖到测试系统中就意味着“我不知道我在做什么,也不知道为什么,如果有什么东西坏了,那肯定不是我!!”;) 它确保可以在新的地方轻松地重新创建数据库(例如,当我们需要从生产中复制一个错误时)。 它极大地提高了DDL文件的质量。

一旦测试开始,内存中的DB就会加载新的数据,在大多数测试之后,我调用ROLLBACK以保持它的稳定。始终保持测试DB中的数据稳定!如果数据一直在变化,就不能进行测试。

数据从SQL、模板DB或转储/备份加载。我更喜欢转储文件,如果它们是可读的格式,因为我可以把它们放在VCS中。如果这不起作用,我就使用CSV文件或XML。如果我必须加载大量的数据……我不喜欢。你永远不需要加载大量的数据:)单元测试不需要。性能测试是另一个问题,适用的规则不同。

我使用第一种方法(针对测试数据库运行代码)。我看到您用这种方法提出的唯一实质性问题是模式不同步的可能性,我通过在数据库中保留一个版本号并通过一个脚本对每个版本增量应用更改来处理这个问题。

我还首先对测试环境进行了所有更改(包括对数据库模式的更改),因此结果正好相反:在所有测试通过之后,将模式更新应用到生产主机。我还在我的开发系统上保留了一对单独的测试数据库和应用程序数据库,这样我就可以在接触真正的生产系统之前验证数据库升级是否正常工作。

即使有允许您以某种方式模拟数据库的工具(例如jOOQ的MockConnection,可以在这个回答中看到-免责声明,我为jOOQ的供应商工作),我也建议不要用复杂的查询模拟大型数据库。

即使您只是想对ORM进行集成测试,也要注意ORM会向数据库发出一系列非常复杂的查询,这些查询可能在

语法 复杂性 订单(!)

模拟所有这些以产生合理的虚拟数据是相当困难的,除非您实际上在模拟中构建了一个小数据库,它解释传输的SQL语句。话虽如此,请使用一个知名的集成测试数据库,您可以很容易地使用知名数据重置该数据库,并针对该数据库运行集成测试。

对于基于JDBC的项目(直接或间接地,例如JPA, EJB,…),您不能模拟整个数据库(在这种情况下,在真正的RDBMS上使用测试数据库会更好),但只能在JDBC级别模拟。

这样做的好处是抽象,因为JDBC数据(结果集、更新计数、警告……)无论在后端是什么:您的prod db、测试db,还是为每个测试用例提供的一些模型数据,都是相同的。

通过为每种情况模拟JDBC连接,不需要管理测试db(清理,一次只进行一个测试,重新加载fixture,等等)。每个模拟连接都是隔离的,不需要清理。每个测试用例中只提供了模拟JDBC交换所需的最低限度的fixture,这有助于避免管理整个测试数据库的复杂性。

Acolyte是我的框架,它包括用于这种模型的JDBC驱动程序和实用程序:http://acolyte.eu.org。

我使用的是第一种方法,但有点不同,可以解决你提到的问题。

为dao运行测试所需的一切都在源代码控制中。它包括创建DB的模式和脚本(docker在这方面非常好)。如果嵌入式数据库可以使用-我使用它的速度。

与其他描述的方法的重要区别在于,测试所需的数据不是从SQL脚本或XML文件加载的。所有东西(除了一些有效的常量字典数据)都是由应用程序使用实用函数/类创建的。

主要目的是使数据用于测试

离考试很近了 显式的(对数据使用SQL文件会使查看哪个测试使用了哪些数据变得非常有问题) 将测试与不相关的更改隔离开来。

这基本上意味着这些实用程序允许声明式地在测试本身中指定对测试至关重要的东西,而忽略不相关的东西。

为了更好地理解它在实践中的含义,可以考虑一些用于由作者编写的帖子注释的DAO的测试。为了测试此类DAO的CRUD操作,应该在DB中创建一些数据。测试看起来像这样:

@Test
public void savedCommentCanBeRead() {
    // Builder is needed to declaratively specify the entity with all attributes relevant
    // for this specific test
    // Missing attributes are generated with reasonable values
    // factory's responsibility is to create entity (and all entities required by it
    //  in our example Author) in the DB
    Post post = factory.create(PostBuilder.post());

    Comment comment = CommentBuilder.comment().forPost(post).build();

    sut.save(comment);

    Comment savedComment = sut.get(comment.getId());

    // this checks fields that are directly stored
    assertThat(saveComment, fieldwiseEqualTo(comment));
    // if there are some fields that are generated during save check them separately
    assertThat(saveComment.getGeneratedField(), equalTo(expectedValue));        
}

与包含测试数据的SQL脚本或XML文件相比,这有几个优点:

维护代码要容易得多(例如,在许多测试中引用的一些实体中添加强制列,如Author,不需要更改大量文件/记录,而只需要更改构建器和/或工厂) 特定测试所需的数据在测试本身中进行了描述,而不是在其他文件中。这种接近性对于测试可理解性非常重要。

回滚vs提交

我发现测试在执行时提交更方便。首先,如果提交从未发生,一些效果(例如DEFERRED CONSTRAINTS)就不能检查。其次,当测试失败时,可以在DB中检查数据,因为回滚没有恢复数据。

Of cause this has a downside that test may produce a broken data and this will lead to the failures in other tests. To deal with this I try to isolate the tests. In the example above every test may create new Author and all other entities are created related to it so collisions are rare. To deal with the remaining invariants that can be potentially broken but cannot be expressed as a DB level constraint I use some programmatic checks for erroneous conditions that may be run after every single test (and they are run in CI but usually switched off locally for performance reasons).