【问题标题】:Unit Testing & Fake Repository implementation with cascading CRUD operations具有级联 CRUD 操作的单元测试和假存储库实施
【发布时间】:2010-05-20 21:29:06
【问题描述】:

我在编写使用假存储库的集成测试时遇到问题,

例如:假设我有一个教室实体,它聚合学生...

var classroom = new Classroom();
classroom.Students.Add(new Student("Adam"));

_fakeRepository.Save(classroom);

_fakeRepostiory.GetAll<Student>().Where((student) => student.Name == "Adam")); 
// This query will return null...

当使用我对存储库的真实实现(基于 NHibernate)时,上面的代码有效(因为保存操作会级联到上一行添加的学生),

你知道任何支持这种行为的虚假存储库实现吗? 关于如何自己实现的想法?

或者你有什么其他建议可以帮助我避免这个问题?

提前致谢, 埃里克。

【问题讨论】:

  • 恕我直言存储库不应更新子实体。对于所有聚合根必须是它们的存储库。

标签: .net unit-testing persistence repository domain-driven-design


【解决方案1】:

如果您正在编写问题正文所表明的“集成测试”,那么您不会使用虚假的实现——使用真实的实现。

如果您正在编写单元测试,正如您的问题的标题所示,您不会关心操作是否级联,因为这不是 Classroom 类的关注点——您只关心Save() 得到在该类作为依赖项提供的任何存储库上调用。

如果您正在编写一个围绕 NHibernate 执行级联操作的能力的单元测试,那么这些测试已经编写完成,并且无法证明 Classroom 类。

根据评论进行编辑:

单元测试用于测试单独的“单元”功能,独立于任何其他类或服务——在这种情况下,您对那些其他类使用假货,以确保它们完全且仅执行该类需要的操作。集成测试用于测试多个类和/或子系统(如数据库访问、ORM 等)之间的交互。在这种情况下,您似乎想测试级联保存/更新,但这是 NHibernate 的责任,不是吗?

但是假设您有一个使用IRepository&lt;Classroom&gt; 保存classroom 的类,并且它将该操作包装在try…catch 块中,并且您想要在IRepository&lt;Classroom&gt; 抛出RepositoryWriteFailureException 时进行单元测试,一个错误计数属性,ErrorCount 被递增。在这里你确实需要你的存储库的假实现,这就是像 RhinoMocks、nMock、Moq 或 TypeMock Isolator 这样的模拟框架发挥作用的地方——这些可以为你生成假的,使用接口、抽象类或已声明公共方法和属性的类virtual.

我可能会这样编写测试主体(将其视为伪代码):

// set up the context (arrange)

// create the fake repository
var fake_repository = MockRepository.GenerateStub<IRepository<Classroom>>();

// create the classroom, injecting the fake repository
dim classroom as new Classroom(fake_repository);

// now tell the fake how to behave
fake_repository.Stub(repo => repo.Save(classroom))
    .WhenCalled(throw new RepositoryWriteFailureException());

// do what you're testing (act)
classroom.Save(); 


// assert the expected behaviour (assert)

// verify that the fake_repository was told to Save(classroom)
Assert.IsTrue(fake_repository.WasToldTo(repo => repo.Save(classroom)).Times(1);

// verify that the error count property was incremented
Assert.IsTrue(classroom.ErrorCount == 1);

请注意我们如何测试 Classroom 类或您正在测试的任何内容的行为方式,甚至无需实现存储库。

【讨论】:

  • 感谢周杰伦的回复!很抱歉混合了单元测试和集成测试这两个术语,上面的代码不是为了测试课堂,这是一种简化,假设我有一个依赖于通用 IRepositoryTEntity> 的服务,作为它调用的逻辑的一部分:_repository .保存(一些聚合)。聚合聚合了其中的几个实体。 NHRepositry 实现支持级联保存操作...我当前的假不...您将如何(单元?集成?不确定正确的术语)测试该服务? (对不起我的英语)埃里克。
  • @Erik 我用更多细节更新了帖子。它的要点是你可以使用一个模拟框架来创建假货,或者如果你自己滚动,只需让它返回值就好像它正在做级联。
  • 谢谢 jay...我很清楚模拟框架... :)) 我遇到问题的信号场景是测试在聚合根上调用 save 的服务类(例如订单) ,然后在稍后的测试中,我必须断言保存了一个特定的订单线......对不起,如果我不是很清楚......
  • @Erik 所以……您没有映射 NHibernate 以将集合与聚合根一起保存?您的存储库类对此负责吗?如果是这样,并且这就是您正在测试的内容,那么您应该使用实际的存储库。如果你真的想测试一个项目是否保存到数据库中,那么你正在做一个集成测试,你应该使用一个实际的数据库(最好是轻量级和内存中的东西,比如 Sqlite)。
  • 是的,我的存储库负责保存整个聚合根及其所有子实体...假设一些服务调用:OrderRepository.Save(order),将保留所有订单的订单行...如果稍后在该方法中会有依赖于订单行的逻辑,那么使用假/模拟/存根存储库会破坏该逻辑,因为它不会级联...感谢杰伊的帮助,但我猜唯一对于这些场景,要走的路是 sqlite(内存模式)+ nhibernate,具有所有含义..
【解决方案2】:

由于它是一个集成测试,我建议您使用真实的存储库,而不仅仅是一个假的。

一个好的解决方案是为测试目的创建一个单独的数据库,并在测试项目的配置文件中将 NHibernate 的hbm2ddl.auto 属性设置为create,这样它每次都会重新创建数据库,所以你不会'也不必担心。

我正是将其用于测试目的。我使用 FluentNHibernate 的自动映射器和自定义约定。如果您想使用比 SQL Server 更轻的东西进行测试,您可能还想使用 SQLite。
(我使用 SQL Server 以这种方式对我的存储库进行单元测试。)

您也可以将其设置为update,如果您希望它保留您已经输入的数据,并且如果您可以手动创建测试数据库,则根本不应该设置它。

【讨论】:

  • 嗨,谢谢你的回复......这是我目前唯一的选择......但问题是我必须在很早的阶段处理持久性(或至少映射),只是执行单元测试...
  • 再次,单元测试和集成测试之间似乎存在混淆。您可以在早期进行单元测试——您伪造存储库行为并且不使用数据库。如果您已准备好与实际存储库实现进行集成测试,那么您必须已经准备好持久性/映射和用于测试的数据库(如果您构建域模型/业务逻辑优先)。
  • @Erik,别让这困扰你。始终测试您的存储库是一件好事。
【解决方案3】:

+1 如果测试不使用真实的东西(即不测试与 RDBMS 的集成),则不将其称为“集成测试”。

如果您想要级联和 NHibernate 的所有其他优点«单元测试»的速度,我发现使用内存数据库 (SQLite) 非常有用。

【讨论】:

  • 感谢您的回复,您使用nhibernate 和内存sqlite 数据库吗?这种方法的唯一缺点是您必须从项目一开始就映射您的实体......(即使流利的 nhibernate 使它非常容易,尤其是它的自动映射功能)或者我错过了什么?
  • 是的,这意味着您必须从一开始就映射您的实体。尽管 NH 和其他 ORM 声称支持 POCO,但这并不是 100% 正确的。因此,如果您知道您将使用 NH,我认为尽早正确地进行映射是一个不错的折衷方案,以弥补失去的领域焦点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-16
  • 2013-01-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多