2

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

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

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

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

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

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

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

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

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

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

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
}
4

4 回答 4

5

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

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

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

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

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

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

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

特别看看:

  • 创造
  • 冻结
  • AutoMock定制

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

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

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

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

你可以做

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

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

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

于 2014-07-23T23:47:58.743 回答
0

考虑使用匿名类型:

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 )。

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

于 2014-07-22T02:19:41.007 回答
0

基本上,诀窍是将尽可能多的混乱推到单元测试之外,以便只保留要测试的行为。

一些方法可以做到这一点:

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

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

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

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

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

就个人而言,我更喜欢 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. */ }
    }
}
于 2014-07-23T18:53:57.703 回答
0

一般来说,如果你很难创建一个简洁的测试,你的测试是错误的,或者你的测试代码有很多责任。(在我的经验中)

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

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

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

于 2014-07-24T02:26:15.853 回答