在unit testing中处理多个模拟和断言

我目前有一个存储库,它使用Entity Framework进行我的CRUD操作。

这是注入我需要使用此repo的服务。

使用AutoMapper,我将实体Model投影到Poco模型上,并且服务返回poco。

如果我的对象有多个属性,那么设置然后断言我的属性的正确方法是什么?

如果我的服务有多个repo依赖项,那么设置我所有模拟的正确方法是什么? * – 一个类[setup],其中为这些测试装置配置了所有模拟和对象?*****

我想避免进行10次测试,每次测试在属性上有50个断言,并且每次测试都有几十个模拟设置。 这使得可维护性和可读性变得困难。

我已经阅读了unit testing艺术,并没有发现任何建议如何处理这种情况。

我使用的工具是Rhino Mocks和NUnit。

我也在SO上找到了这个,但它没有回答我的问题: 正确的unit testing服务/存储库交互

这是一个表达我所描述内容的示例:

public void Save_ReturnSavedDocument() { //Simulate DB object var repoResult = new EntityModel.Document() { DocumentId = 2, Message = "TestMessage1", Name = "Name1", Email = "Email1", Comment = "Comment1" }; //Create mocks of Repo Methods - Might have many dependencies var documentRepository = MockRepository.GenerateStub(); documentRepository.Stub(m => m.Get()).IgnoreArguments().Return(new List() { repoResult }.AsQueryable()); documentRepository.Stub(a => a.Save(null, null)).IgnoreArguments().Return(repoResult); //instantiate service and inject repo var documentService = new DocumentService(documentRepository); var savedDocument = documentService.Save(new Models.Document() { ID = 0, DocumentTypeId = 1, Message = "TestMessage1" }); //Assert that properties are correctly mapped after save Assert.AreEqual(repoResult.Message, savedDocument.Message); Assert.AreEqual(repoResult.DocumentId, savedDocument.DocumentId); Assert.AreEqual(repoResult.Name, savedDocument.Name); Assert.AreEqual(repoResult.Email, savedDocument.Email); Assert.AreEqual(repoResult.Comment, savedDocument.Comment); //Many More properties here } 

首先,每个测试应该只有一个断言(除非另一个validation真实的断言)eq如果你想断言列表的所有元素都是不同的,你可能想先断言列表不是空的。 否则你可能会得到误报。 在其他情况下,每个测试应该只有一个断言。 为什么? 如果测试失败,它的名字会告诉你到底出了什么问题。 如果你有多个断言并且第一个失败,你不知道其余的是否正常。 你所知道的只是“出了问题”。

你说你不想在10次测试中设置所有的模拟/存根。 这就是大多数框架为您提供在每次测试之前运行的安装方法的原因。 在这里,您可以将大多数模拟配置放在一个位置并重复使用。 在NUnit中,您只需创建一个方法并使用[SetUp]属性进行装饰。

如果要测试具有不同参数值的方法,可以使用NUnit的[TestCase]属性。 这非常优雅,您不必创建多个相同的测试。

现在让我们谈谈有用的工具。

AutoFixture这是一个非常强大且非常强大的工具,它允许您创建需要多个依赖项的类的对象。 它会自动设置与虚拟模拟的依赖关系,并允许您仅手动设置特定测试中所需的模拟。 假设您需要为UnitOfWork创建一个模拟器,它将10个存储库作为依赖项。 在您的测试中,您只需要设置其中一个。 Autofixture允许您创建UnitOfWork,设置一个特定的存储库模拟(如果需要,可以设置更多)。 其余的依赖项将使用虚拟模拟自动设置。 这为您节省了大量无用的代码。 它有点像你的测试的IOC容器。

它还可以为您生成带有随机数据的虚假对象。 所以eq整个EntityModel.Document的初始化只是一行

 var repoResult = _fixture.Create(); 

特别看看:

  • 创建
  • 冻结
  • AutoMockCustomization

在这里,您将找到解释如何使用AutoFixture的答案。

SemanticComparison 教程这将帮助您在比较不同类型的对象的属性时避免多个断言。 如果属性具有相同的名称,它几乎会自动生成。 如果没有,您可以定义映射。 它还会告诉您哪些属性不匹配并显示其值。

流畅的断言这只是为您提供一种更好的断言方式。 代替

 Assert.AreEqual(repoResult.Message, savedDocument.Message); 

你可以做

 repoResult.Message.Should().Be(savedDocument.Message); 

总结一下。 这些工具将帮助您使用更少的代码创建测试,并使它们更具可读性。 要好好了解它们需要时间。 特别是AutoFixture,但是当你这样做时,它们会成为你添加到测试项目中的第一件事 – 相信我:)。 顺便说一句,他们都可以从Nuget购买。

还有一个提示。 如果您在测试类时遇到问题,通常会表明架构不正确。 解决方案通常是从有问题的类中提取较小的类。 (单一责任主体)您可以轻松地测试小类的业务逻辑。 并轻松测试原始类与它们的交互。

考虑使用匿名类型:

 public void Save_ReturnSavedDocument() { // (unmodified code)... //Assert that properties are correctly mapped after save Assert.AreEqual( new { repoResult.Message, repoResult.DocumentId, repoResult.Name, repoResult.Email, repoResult.Comment, }, new { savedDocument.Message, savedDocument.DocumentId, savedDocument.Name, savedDocument.Email, savedDocument.Comment, }); } 

有一件事需要注意:可空类型(例如int?)和可能具有稍微不同类型的属性(float vs double) – 但是您可以通过将属性转换为特定类型来解决此问题(例如。(int?) repoResult.DocumentId)。

另一种选择是创建自定义断言类/方法。

基本上,诀窍是在unit testing之外尽可能多地推动混乱,这样只有待测试的行为仍然存在。

一些方法:

  1. 不要在每个测试中声明模型/ poco类的实例,而是使用将这些实例公开为属性的静态TestData类。 通常这些实例也可用于多个测试。 为了增加健壮性,让TestData类的属性在每次访问时创建并返回一个新的对象实例,这样一个unit testing就不会通过修改testdata来影响下一个。

  2. 在您的测试类上,声明一个帮助方法,该方法接受(通常是模拟的)存储库并返回被测系统(或“SUT”,即您的服务)。 这在配置SUT超过2个或更多语句的情况下非常有用,因为它可以整理您的测试代码。

  3. 作为2的替代方法,让您的testclass公开每个模拟存储库的属性,这样您就不需要在unit testing中声明这些属性; 您甚至可以使用默认行为对它们进行预初始化,以进一步减少每单位测试的配置。
    然后,返回SUT的辅助方法不会将模拟的存储库作为参数,而是使用属性来构造SUT。 您可能希望在每个[TestInitialize]上重新初始化每个Repository属性。

  4. 为了减少将Poco的每个属性与Model对象上的相应属性进行比较的混乱,请在测试类上声明一个帮助您执行此操作的辅助方法(即void AssertPocoEqualsModel(Poco p, Model m) )。 再次,这消除了一些混乱,你可以免费获得可重用性。

  5. 或者,作为4的替代,不要比较每个unit testing中的所有属性,而是仅使用一组单独的unit testing在一个地方测试映射代码。 这具有额外的好处,如果映射包括新属性或以任何其他方式更改,则不必更新100多个unit testing。
    在不测试属性映射时,您应该只validationSUT返回正确的对象实例(即基于IdName ),并且只有可能更改的属性(通过当前测试的业务逻辑)包含正确的值(如订单总额)。

就个人而言,我更喜欢5,因为它的可维护性,但这并不总是可行的,然后4通常是一个可行的选择。

您的测试代码将如下所示(未经validation,仅用于演示目的):

 [TestClass] public class DocumentServiceTest { private IDocumentRepository DocumentRepositoryMock { get; set; } [TestInitialize] public void Initialize() { DocumentRepositoryMock = MockRepository.GenerateStub(); } [TestMethod] public void Save_ReturnSavedDocument() { //Arrange var repoResult = TestData.AcmeDocumentEntity; DocumentRepositoryMock .Stub(m => m.Get()) .IgnoreArguments() .Return(new List() { repoResult }.AsQueryable()); DocumentRepositoryMock .Stub(a => a.Save(null, null)) .IgnoreArguments() .Return(repoResult); //Act var documentService = CreateDocumentService(); var savedDocument = documentService.Save(TestData.AcmeDocumentModel); //Assert that properties are correctly mapped after save AssertEntityEqualsModel(repoResult, savedDocument); } //Helpers private DocumentService CreateDocumentService() { return new DocumentService(DocumentRepositoryMock); } private void AssertEntityEqualsModel(EntityModel.Document entityDoc, Models.Document modelDoc) { Assert.AreEqual(entityDoc.Message, modelDoc.Message); Assert.AreEqual(entityDoc.DocumentId, modelDoc.DocumentId); //... } } public static class TestData { public static EntityModel.Document AcmeDocumentEntity { get { //Note that a new instance is returned on each invocation: return new EntityModel.Document() { DocumentId = 2, Message = "TestMessage1", //... } }; } public static Models.Document AcmeDocumentModel { get { /* etc. */ } } } 

一般来说,如果您很难创建简洁的测试,那么您的测试错误的东西或您的测试代码将承担很多责任。 (在我的经验中)

具体来说,看起来你在这里测试错误的东西。 如果您的repo正在使用entity framework,那么您将获得与发送相同的对象.Ef只会更新Id以获取新对象和您可能拥有的任何时间戳字段。

此外,如果在没有第二个断言失败的情况下无法使一个断言失败,那么您不需要其中一个断言。 “名称”真的可以回来,但“电子邮件”失败了吗? 如果是这样,他们应该进行单独的测试。

最后,尝试做一些tdd可能会有所帮助。 在你的service.save中注释掉所有可能的内容。 然后,编写一个失败的测试。 然后取消注释只有足够的代码让你的测试通过。 他们写下你的下一个失败的测试。 不能写一个失败的测试? 然后你完成了。