代码之家  ›  专栏  ›  技术社区  ›  friedo

单元测试数据库驱动的应用程序的最佳策略是什么?

  •  291
  • friedo  · 技术社区  · 16 年前

    我使用了许多Web应用程序,这些应用程序由后端复杂度不同的数据库驱动。通常,有一个 ORM 与业务和表示逻辑分离的层。这使得单元测试业务逻辑相当简单;事情可以在离散的模块中实现,测试所需的任何数据都可以通过对象模拟来伪造。

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

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

    • 使用已知数据加载测试数据库。对ORM运行测试并确认返回正确的数据。这里的缺点是测试数据库必须跟上应用程序数据库中的任何模式更改,并且可能会失去同步。它还依赖于人工数据,并且可能不会暴露由于愚蠢的用户输入而发生的错误。最后,如果测试数据库很小,它就不会显示效率低下,比如索引丢失。(好吧,最后一个不是单元测试的真正用途,但它不会造成伤害。)

    • 加载生产数据库的一个副本,并根据该副本进行测试。这里的问题是,在任何给定的时间,您可能都不知道生产数据库中有什么;如果数据随时间变化,可能需要重写测试。

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

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

    您在测试数据库驱动的应用程序(如果有的话)时使用了什么策略?什么对你最有效?

    7 回复  |  直到 6 年前
        1
  •  134
  •   Ivan Nevostruev    15 年前

    实际上,我已经成功地使用了你的第一种方法,但是我认为用一种稍有不同的方式来解决你的一些问题:

    1. 保留用于在源代码管理中创建它的整个架构和脚本,以便任何人都可以在签出后创建当前数据库架构。此外,将示例数据保存在由构建过程的一部分加载的数据文件中。当您发现导致错误的数据时,将其添加到示例数据中,以检查错误是否不会再次出现。

    2. 使用持续集成服务器来构建数据库模式、加载示例数据和运行测试。这就是我们保持测试数据库同步的方法(在每次测试运行时重建它)。尽管这要求CI服务器拥有自己专用数据库实例的访问权限和所有权,但我认为,每天构建3次DB模式极大地有助于发现错误,这些错误可能在交付之前(如果不是稍后)才被发现。我不能说我在每次提交之前都要重新构建模式。有人吗?有了这种方法,你就不必这么做了(好吧,也许我们应该这样做,但如果有人忘了,那就没什么大不了了)。

    3. 对于我的组,用户输入是在应用程序级别(而不是数据库)完成的,因此这是通过标准单元测试来测试的。

    正在加载生产数据库副本:
    这是我上次工作时采用的方法。这是由以下几个问题引起的巨大痛苦:

    1. 副本将从生产版本中过期
    2. 将对副本的模式进行更改,并且不会传播到生产系统。在这一点上,我们会有不同的模式。不好玩。

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

        2
  •  51
  •   Aaron Digulla    16 年前

    我总是针对内存中的数据库(hsqldb或derby)运行测试,原因如下:

    • 它让您考虑将哪些数据保存在测试数据库中以及原因。把生产数据库拖到测试系统中就意味着“我不知道我在做什么,也不知道为什么,如果有什么东西坏了,那不是我!!“;)
    • 它可以确保在新的地方轻松地重新创建数据库(例如,当我们需要从生产中复制错误时)
    • 它对DDL文件的质量有很大帮助。

    一旦测试开始,内存中的数据库就会装载新的数据,在大多数测试之后,我调用rollback来保持它的稳定。 总是 保持测试数据库中的数据稳定!如果数据一直在变化,则无法测试。

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

        3
  •  13
  •   kolrie    16 年前

    我问这个问题已经有很长时间了,但我认为没有什么好办法。

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

    我用这种方法看到的主要问题是,您只覆盖与DAO层交互的代码,但从不测试DAO本身,根据我的经验,在该层上也会发生很多错误。我还保留了一些针对数据库运行的单元测试(为了在本地使用TDD或快速测试),但这些测试从不在我的持续集成服务器上运行,因为我们不为此保留数据库,我认为在CI服务器上运行的测试应该是自包含的。

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

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

    无论您认为什么与您的代码更相关,都有一些项目可以使它变得更容易,比如 DbUnit .

        4
  •  9
  •   Community CDub    7 年前

    即使有工具允许您以某种方式模拟数据库(例如 jOOQ MockConnection ,可以在 this answer -免责声明,我为Jooq的供应商工作),我建议 用复杂的查询模拟更大的数据库。

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

    • 句法
    • 复杂性
    • 命令(!)

    模拟所有这些以生成明智的虚拟数据是非常困难的,除非您实际上在模拟中构建了一个小数据库,它解释了传输的SQL语句。这么说之后,使用一个众所周知的集成测试数据库,您可以很容易地用众所周知的数据重置它,然后根据它运行集成测试。

        5
  •  4
  •   Dave Sherohman    16 年前

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

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

        6
  •  1
  •   Roman-Stop RU aggression in UA    6 年前

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

    运行DAOS测试所需的一切都在源代码管理中。它包括创建数据库的模式和脚本(Docker非常适合这样做)。如果可以使用嵌入式数据库-我使用它来提高速度。

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

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

    1. 离测试非常近
    2. 显式(对数据使用SQL文件会使查看哪些数据被哪些测试使用变得非常困难)
    3. 将测试与不相关的更改隔离开来。

    它基本上意味着这些实用程序只允许声明性地指定测试本身所必需的东西,并省略不相关的东西。

    为了了解它在实践中的含义,可以考虑使用 Comment S to Post 书面的 Authors .为了测试这种DAO的CRUD操作,应该在数据库中创建一些数据。测试结果如下:

    @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文件相比,这有几个优点:

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

    是否回滚

    我发现测试在执行时提交更方便。首先,一些影响(例如 DEFERRED CONSTRAINTS )如果从未发生提交,则无法检查。其次,当测试失败时,可以在数据库中检查数据,因为数据不会被回滚还原。

    原因是,这有一个缺点,即测试可能会产生损坏的数据,这将导致其他测试中的失败。为了解决这个问题,我尝试隔离测试。在上面的示例中,每个测试都可以创建新的 Author 所有其他实体都是与之相关的,所以很少发生碰撞。为了处理可能被破坏但不能表示为db级约束的剩余不变量,我使用一些程序检查在每次测试后可能运行的错误条件(它们在CI中运行,但由于性能原因通常在本地关闭)。

        7
  •  1
  •   cchantep    6 年前

    对于基于JDBC的项目(直接或间接,例如JPA、EJB等),您可以模拟整个数据库(在这种情况下,最好在真实的RDBMS上使用测试数据库),但只能在JDBC级别模拟。

    优势在于抽象,它是以这种方式提供的,因为JDBC数据(结果集、更新计数、警告等)与后端的数据是相同的:您的产品数据库、测试数据库,或者只是为每个测试用例提供的一些模拟数据。

    对于每种情况,JDBC连接都是模拟的,因此不需要管理测试数据库(清理、一次仅一个测试、重新加载设备…)。每个模型连接都是孤立的,不需要清理。在每个测试用例中,只提供了模拟JDBC交换所需的最少的设备,这有助于避免管理整个测试数据库的复杂性。

    Acolyte是我的框架,其中包括用于此类模型的JDBC驱动程序和实用程序: http://acolyte.eu.org .