【问题标题】:Handling Multiple Mocks and Asserts in Unit Tests在单元测试中处理多个模拟和断言
【发布时间】:2014-09-07 20:37:49
【问题描述】:

我目前有一个使用实体框架进行 CRUD 操作的存储库。

这被注入到我需要使用这个 repo 的服务中。

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

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

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

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

我已经阅读了单元测试的艺术,但没有发现任何关于如何处理这种情况的建议。

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

我也在 SO 上找到了这个,但它没有回答我的问题:Correctly Unit Test Service / Repository Interaction

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

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<IDocumentRepository>();
    documentRepository.Stub(m => m.Get()).IgnoreArguments().Return(new List<EntityModel.Document>()
        {
           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
}

【问题讨论】:

  • 除了属性映射之外,您要测试的逻辑是否正常工作?
  • 此时测试映射是否正确,验证是否正常工作,不会对有效数据抛出任何验证异常。
  • SetSeeker - 不要测试“任何”验证,测试“a”验证然后测试下一个。请参阅下面的更多详细信息。
  • 可读性对于单元测试很重要,但可维护性和 DRY 则不是。最好有很多测试,每个测试一件事,最好用 1 个断言。

标签: c# unit-testing nunit repository-pattern rhino-mocks-3.5


【解决方案1】:

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

您说您不想在 10 个测试中设置所有模拟/存根。这就是为什么大多数框架都会为您提供在每次测试之前运行的 Setup 方法。在这里,您可以将大部分模拟配置放在一个地方并重用它。在 NUnit 中,您只需创建一个方法并用 [SetUp] 属性装饰它。

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

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

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

它还可以为您生成带有随机数据的假对象。所以等式EntityModel.Document 的整个初始化过程只需一行

var repoResult = _fixture.Create<EntityModel.Document>();

特别看看:

  • 创建
  • 冻结
  • AutoMock 自定义

Here你会发现我的答案解释了如何使用 AutoFixture。

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

Fluent assertions 这只是为您提供了一种更好的断言方式。 而不是

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

你可以的

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

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

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

【讨论】:

    【解决方案2】:

    考虑使用匿名类型:

    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 与 double) - 但您可以通过将属性转换为特定类型(例如,( int?)repoResult.DocumentId).

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

    【讨论】:

      【解决方案3】:

      基本上,诀窍是将尽可能多的混乱推到单元测试之外,这样只有行为 待测试的遗骸。

      一些方法:

      1. 不要在每个测试中声明模型/poco 类的实例,而是使用静态 TestData 类 将这些实例公开为属性。通常这些实例也可用于不止一项测试。 为了增加稳健性,让 TestData 类的属性每次都创建并返回一个新的对象实例 它们被访问,因此一个单元测试不能通过修改测试数据来影响下一个单元测试。

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

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

      4. 1234563同样,这消除了一些混乱,您可以免费获得可重用性。
      5. 或者,作为 4 的替代方案,不要比较每个单元测试中的所有属性,而是使用一组单独的单元测试仅在一个地方测试映射代码。这有一个额外的好处,即如果映射包含新属性或更改 以任何其他方式,您不必更新 100 多个单元测试。
        当不测试属性映射时,您应该只验证 SUT 返回正确的对象实例(即基于IdName),以及可能更改的属性(通过当前正在测试的业务逻辑) 包含正确的值(例如订单总额)。

      我个人更喜欢 5,因为它的可维护性,但这并不总是可行的,然后 4 通常是一个可行的替代方案。

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

      [TestClass]
      public class DocumentServiceTest
      {
          private IDocumentRepository DocumentRepositoryMock { get; set; }
      
          [TestInitialize]
          public void Initialize()
          {
              DocumentRepositoryMock = MockRepository.GenerateStub<IDocumentRepository>();
          }
      
          [TestMethod]
          public void Save_ReturnSavedDocument()
          {
              //Arrange
              var repoResult = TestData.AcmeDocumentEntity;
      
              DocumentRepositoryMock
                  .Stub(m => m.Get())
                  .IgnoreArguments()
                  .Return(new List<EntityModel.Document>() { 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. */ }
          }
      }
      

      【讨论】:

        【解决方案4】:

        一般来说,如果您在创建简洁的测试时遇到困难,那么您测试错误的东西或您测试的代码需要承担很多责任。 (根据我的经验)

        具体来说,您似乎在这里测试了错误的东西。如果您的 repo 使用实体框架,您将获得与您发送的相同的对象。Ef 只是更新到 Id 以获取新对象和您可能拥有的任何时间戳字段。

        另外,如果你不能让你的一个断言在没有第二个断言失败的情况下失败,那么你不需要其中一个。 “姓名”真的有可能恢复正常但“电子邮件”失败吗?如果是这样,它们应该在单独的测试中。

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-06-17
          • 1970-01-01
          • 2011-02-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多