单元测试Unit Testing [51]

10.5 常见的数据库测试问题

  在本章的最后一节,我想回答与数据库测试有关的常见问题,并简要重申第8章和第9章中的一些重要观点。

10.5.1 你应该测试读取操作吗?

  在过去的几章中,我们已经使用了一个更改用户电子邮件的示例场景。这个场景是一个写操作的例子(一个会在数据库和其他进程外的依赖中留下副作用的操作)。 大多数应用程序都包含写和读操作。读操作的一个例子是将用户信息返回给外部客户。你应该同时测试写和读吗?
  彻底测试写操作是很关键的,因为风险很大。写入操作的错误往往会导致数据损坏,这不仅会影响你的数据库,也会影响外部应用程序。涵盖写操作的测试是非常有价值的,因为它们提供了对这种错误的保护。
  读取的情况则不同:读取操作中的错误通常不会产生如此有害的后果。因此,测试读的门槛应该比写的门槛高。只测试最复杂或最重要的读操作;不考虑其他的。
  请注意,在读取操作中也不需要领域模型。领域建模的主要目标之一是封装。而且,你可能还记得第5章和第6章,封装是为了在任何变化中保持数据的一致性。缺乏数据变化使得对读的封装毫无意义。事实上,你也不需要一个成熟的ORM,如NHibernate或Entity Framework,在读取中。你最好使用普通的SQL,由于绕过了不必要的抽象层,它的性能优于ORM(图10.7)。

单元测试Unit Testing [51] 图10.7 读取时不需要领域模型。而且,由于读中的错误成本比写中的低,所以也不需要进行集成测试。

 
  因为在reads中几乎没有任何抽象层(领域模型就是这样一个层),单元测试在那里没有任何用处。如果你决定测试你的读取,请使用真实数据库上的集成测试。

10.5.2 你应该测试存储库吗?

  存储库在数据库的基础上提供了一个有用的抽象概念。下面是我们的CRM项目样本中的一个使用例子。

User user = _userRepository.GetUserById(userId);
_userRepository.SaveUser(user);

  你应该独立于其他集成测试来测试存储库吗? 测试存储库如何将域对象映射到数据库中似乎是有益的。 毕竟,在这个功能上有很大的错误空间。然而,这样的测试对你的测试套件来说是一个净损失,因为维护成本高,对回退的保护程度低。让我们更详细地讨论这两个缺点。

高维护成本

  存储库属于第七章代码类型图中的控制者象限(图10.8)。它们表现出很小的复杂性,并与进程外的依赖关系进行交流:数据库。这种进程外的依赖性的存在,使测试的维护成本上升。
  当涉及到维护成本时,测试资源库与常规的集成测试有着同样的负担。但是,这样的测试是否提供了同等数量的回报?不幸的是,它并没有。

单元测试Unit Testing [51] 图10.8 存储库表现出很少的复杂性,并与进程外的依赖关系进行通信,因此属于代码类型图中的控制器象限。

 
防止回退的低级保护措施
  存储库没有那么多的复杂性,而且在保护回归方面的许多收益与常规的集成测试所提供的收益相重叠。因此,对存储库的测试并没有增加足够大的价值。
  测试一个资源库的最好方法是把它的一点复杂性提取到一个独立的算法中,然后专门测试这个算法。 这就是前几章中UserFactory和CompanyFactory的作用。这两个类在没有任何合作者的情况下,对所有的映射进行运算,不管是进程外的还是其他的。存储库(数据库类)只包含简单的SQL查询。
  不幸的是,在使用ORM时,数据映射(以前由工厂执行)和与数据库的交互(以前由数据库执行)之间的这种分离是不可能的。 你不能在不调用数据库的情况下测试你的ORM映射,至少不能在不影响抗重构的情况下测试。因此,坚持以下准则:不要直接测试存储库,只作为总体集成测试套件的一部分
  也不要单独测试EventDispatcher(该类将领域事件转换为对非管理依赖的调用)。由于维护复杂的模拟机制所需的成本太高,在防止回退方面的收益太少。

10.6 结论

  针对数据库的精心设计的测试提供了对错误的防弹保护。 根据我的经验,它们是最有效的工具之一,没有它们,就不可能对你的软件获得充分的信心。当你重构数据库,更换ORM,或改变数据库供应商时,这样的测试有很大的帮助。
  事实上,在本章的前面,我们的示例项目已经过渡到Entity Framework ORM,我只需要在集成测试中修改几行代码就可以确保过渡成功了。直接与被管理的依赖关系一起工作的集成测试是防止大规模重构导致的错误的最有效方法。

纪要

  • 将数据库模式与你的源代码一起存储在源控制系统中。数据库模式由表、视图、索引、存储过程和其他任何构成数据库构造蓝图的东西组成。
  • 参考数据也是数据库模式的一部分。它是必须预先填充的数据,以使应用程序能够正常运行。 要区分参考数据和常规数据,要看你的应用程序是否可以修改这些数据。如果可以,它就是常规数据;否则,它就是参考数据。
  • 为每个开发人员准备一个单独的数据库实例。 更好的是,将该实例托管在开发人员自己的机器上,以获得最大的测试执行速度。
  • 基于状态的数据库交付方法使状态明确,并让比较工具隐含地控制迁移。 基于迁移的方法强调使用明确的迁移,将数据库从一个状态过渡到另一个状态。数据库状态的明确性使其更容易处理合并冲突,而明确的迁移则有助于解决数据运动问题。
  • 与基于状态的方法相比,更倾向于基于迁移的方法,因为处理数据运动比合并冲突要重要得多。通过迁移来应用对数据库模式的每一次修改(包括参考数据)。
  • 业务操作必须原子化地更新数据。为了实现原子性,要依靠底层数据库的事务机制。
  • 在可能的情况下使用工作单元模式。工作单元依赖于底层数据库的事务;它还将所有的更新推迟到业务操作的结束,从而提高性能。
  • 不要在测试的各个部分之间重复使用数据库事务或工作单元。每个安排、行动和断言部分都应该有自己的事务或工作单元。
  • 按顺序执行集成测试。 并行执行涉及大量的工作,通常不值得。
  • 在测试开始时清理剩余的数据。 这种方法见效快,不会导致不一致的行为,而且不容易意外地跳过清理阶段。使用这种方法,你也不必引入一个单独的拆解阶段。
  • 避免使用内存数据库,如SQLite。如果你的测试针对不同供应商的数据库运行,你将永远无法获得良好的保护。在测试中使用与生产中相同的数据库管理系统。
  • 通过将非必要的部分提取到私有方法或辅助类中来缩短测试:
    • 对于安排部分,在测试数据生成器上选择Object Mother。
    • 对于行为,创建装饰器方法。
    • 对于断言,引入一个流畅的接口
  • 测试读的阈值应该比写的阈值高。只测试最复杂或最重要的读操作;不考虑其他的。
  • 不要直接测试存储库,而只是作为总体集成测试套件的一部分。 对存储库的测试引入了太高的维护成本,而在保护回归方面的额外收益太少。
上一篇:Django(20)ORM模型迁移命令


下一篇:JWT(json web token)