C# 在单元测试中处理多个模拟和断言

C# 在单元测试中处理多个模拟和断言,c#,unit-testing,nunit,repository-pattern,rhino-mocks-3.5,C#,Unit Testing,Nunit,Repository Pattern,Rhino Mocks 3.5,我目前有一个使用Entity Framework进行CRUD操作的存储库 这是注入到我的服务,需要使用此回购 使用AutoMapper,我将实体模型投影到Poco模型上,Poco由服务返回 如果我的对象有多个属性,那么设置并断言我的属性的正确方法是什么? 如果我的服务有多个repo依赖项,那么设置所有模拟的正确方法是什么*-一个类[设置],其中为这些测试装置配置了所有模拟和对象***** 我希望避免有10个测试,每个测试有50个属性断言和几十个模拟设置。这使得可维护性和可读性变得困难 我读过单元

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

这是注入到我的服务,需要使用此回购

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

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

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

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

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

我使用的工具是Rhino Mocks和NUnit

我也发现了这个,但它没有回答我的问题:

下面是一个示例,它表达了我所描述的内容:

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
}
public void Save\u ReturnSavedDocument()
{
//模拟数据库对象
var reposult=new EntityModel.Document()
{
DocumentId=2,
Message=“TestMessage1”,
Name=“Name1”,
Email=“Email1”,
Comment=“Comment1”
};
//创建Repo方法的模拟-可能有许多依赖项
var documentRepository=MockRepository.GenerateStub();
documentRepository.Stub(m=>m.Get()).IgnoreArguments().Return(new List())
{
报告结果
}.AsQueryable());
documentRepository.Stub(a=>a.Save(null,null)).IgnoreArguments().Return(reposult);
//实例化服务并注入repo
var documentService=新的documentService(documentRepository);
var savedDocument=documentService.Save(新模型.Document()
{
ID=0,
DocumentTypeId=1,
Message=“TestMessage1”
});
//断言属性在保存后已正确映射
Assert.AreEqual(reposult.Message、savedDocument.Message);
Assert.AreEqual(reposult.DocumentId、savedDocument.DocumentId);
Assert.AreEqual(reposult.Name、savedDocument.Name);
Assert.AreEqual(reposult.Email、savedDocument.Email);
Assert.AreEqual(reposult.Comment、savedDocument.Comment);
//这里还有很多房产
}

考虑使用匿名类型:

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,
        });
}
有一件事需要注意:可为null的类型(例如int?)和可能具有稍微不同类型的属性(float vs double)-但您可以通过将属性强制转换为特定类型(例如(int?)reposult.DocumentId)来解决这一问题


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

基本上,技巧是将尽可能多的混乱推到unittests之外,这样只有 这有待检验

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

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

  • 在testclass上,声明一个helper方法,该方法接受(通常是模拟的)存储库并返回 正在测试的系统(或“SUT”,即您的服务)。这主要适用于配置SUT的情况 接受2个或更多语句,因为它会整理测试代码

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

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

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

  • 就个人而言,我更喜欢5,因为它的可维护性,但这并不总是可能的,而4通常是一个可行的替代方案

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

    [TestClass]
    公共类DocumentServiceTest
    {
    私有IDocumentrepositorymock{get;set;}
    [测试初始化]
    公共无效初始化()
    {
    DocumentRepositoryMock=MockRepository.GenerateSub();
    }
    [测试方法]
    public void Save_ReturnSavedDocument()
    {
    //安排
    var reposult=TestData.AcmeDocumentEntity;
    DocumentRepositoryMock
    .Stub(m=>m.Get())
    .IgnoreArguments()
    
    [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. */ }
        }
    }
    
    var repoResult = _fixture.Create<EntityModel.Document>();
    
    Assert.AreEqual(repoResult.Message, savedDocument.Message);
    
    repoResult.Message.Should().Be(savedDocument.Message);