避免重复单元测试的测试数据的正确方法

时间:2013-09-24 14:52:36

标签: c# entity-framework unit-testing tdd moq

假设我使用Moq进行了以下针对实体框架6的单元测试:

    public void Save_Employee_via_context()
    {
        var MockContext = new Mock<DcmDataContext>();

        var MockSet = new Mock<DbSet<Employee>>();

        MockContext.Setup(m => m.Employees).Returns(MockSet.Object);

        var service = new GeneralService(MockContext.Object);

        //Test valid inputs

        for (int i = 0; i < TestData.ValidEmployees.Count; i++)
        {
            service.AddEmployee(TestData.ValidEmployees[i]);
            //veryfy that it was properly inserted
            Assert.AreEqual(TestData.ValidEmployees[i],MockSet.Object.Find(TestData.ValidEmployees[i].EmployeeID));
        }
        //Ensure that the proper methods were called each time.  It is implied that this happened if the above 
        //Assert methods passed, but double checking never killed anybody
        MockSet.Verify(m => m.Add(It.IsAny<Employee>()), Times.Exactly(TestData.ValidEmployees.Count));
        MockContext.Verify(m => m.SaveChanges(), Times.Exactly(TestData.ValidEmployees.Count));

        //Test invalid Inputs

        MockSet = new Mock<DbSet<Employee>>();
        //InvalidEmployees is a Dictionary<Employee,Type>, where Type is the type of exeption that should eb thrown if 
        //You attempt to add that Employee
        foreach (var pair in TestData.InvalidEmployees)
        {
            try
            {
                service.AddEmployee(pair.Key);
                //AddEmployee *SHOULD* throw an exception here here.. if not...
                Assert.Fail();
            }
            catch (Exception ex)
            {
                //Was it the exception that I was expecting to catch?
                Assert.Equals(ex.GetType(), pair.Value);
            }
        }
        //ensure that nothing new has been added (redundant, I know, but it doesn't hurt)
        MockSet.Verify(m => m.Add(It.IsAny<Employee>()), Times.Never);
        MockContext.Verify(m => m.SaveChanges(), Times.Exactly(TestData.ValidEmployees.Count));

    }

TestData是一个静态类,它包含我要测试的每个模型类型的列表,以及每个模型的几个测试用例,包括有效和无效的输入。

我创建了这样的测试,因为我的对象可能相当大(Employee,例如,有大约15个属性),因此我想按顺序运行各种测试用例每次测试都要彻底。我不想复制/粘贴每个需要它的方法的每个测试样本数据数组,因此我想将它存储在静态容器中。

但是,我觉得这会带来问题。例如,Employee的其中一个属性是Position。你知道,他们有什么工作。它是必需属性,如果该位置为null或数据库中尚不存在,则应抛出异常。这意味着为了使上述测试有效,我将需要一些模拟位置。哦,但每个职位都有一个Department属性......所以也需要设置......

你知道我要去哪儿吗?如何在没有全套测试数据的情况下对我的代码进行正确测试以对其进行测试?那么,我想我将不得不编写一整套测试数据。我做了什么。

问题是,我在哪里放?我决定将所有内容放入TestData类中 然而,这提出了一系列问题。初始化是最大的,因为我觉得我必须中断我的测试数据,以便进行初始化甚至远程可行。例如,我的所有导航属性可能都必须是null。我怎么能让ValidEmployees每个List<Clients>都有一个Client,而每个Employee都有一个Client分配List<Employee>,再一次,难以将每个员工复制为{{1}的属性在每个Position将要拥有的Clients = {ValidClients[0],ValidClients[1]中。在ValidClients中的ValidEmployeesSalesRepresentative = ValidEmployees[0] Assert.AreEqual ( TestData.ValidEmployees[i], MockSet.Object.Find(TestData.ValidEmployees[i].EmployeeID ) 不是很好吗?

我也觉得我需要导航数据。将

{{1}}
如果ValidEmployees中没有navigationData,

仍然返回true?这是否意味着我应该找到另一种确保国家的方式?

无论如何,这些都是我遇到的问题。我刚刚设置我的单元测试完全错了吗?我还应该如何获得强大,独立,干燥和准确的单元测试?我在这里缺少什么?

任何帮助都表示赞赏,即使这意味着从头开始,心态不同。这是我的第一个项目,我非常认真地对待测试,但我觉得它不是那么顺利。因此,抱歉文本墙。有时我觉得我没有问正确的问题到达我想去的地方。

2 个答案:

答案 0 :(得分:2)

回答问题的第二部分(关于测试数据)。我不会将测试数据类用于我的测试,这将使测试变得脆弱,并且可能会引入细微的错误,因为测试数据类的更改可能会影响不同测试类中的众多无关测试,google Object Mother Anti-Pattern。

我之前走了Object Mother路线,最后得到了一个包含我所有测试数据的整个项目。当我需要测试数据的新变体时,我一直在改变/添加对象母亲。毋庸置疑,该项目很快就变得臃肿且难以维护。此外,由于这些变化(由于存在对此共享数据的依赖性),一些单元测试开始失败的事实以及修复它们所花费的额外时间使得它成为一个真正的烦恼。因此,我认为更好的方法是让单元测试类拥有他们使用的数据(换句话说,测试数据包含在测试夹具中)。为了实现这一点,我介绍了一个测试数据构建器项目,但这意味着每当我编写单元测试时,我都有另一个项目来维护和更改(在需要时)。老实说,我宁愿专注于单元测试本身,也不用担心管道问题,这就是我开始使用测试数据生成器的原因。我正在使用NBuilder,但我也听到了关于AutoFixture的非常好的事情。这些将允许您构建测试数据,专注于与您正在测试的行为相关的部分,并让构建器生成随机数据(您可以控制/覆盖随机生成器)。我认为,为了提高单元测试的可读性,您应该只显示(或强调)影响您正在测试的行为的数据,而不是使用无关信息使您的单元测试膨胀。

示例:

var validEmployees = Builder<Employees>.CreateListOfSize(10)
                                       .All()
                                       .With(x => x.IsActive = true)
                                       .And(x => x.LeaveDate = null)
                                       .Build();

答案 1 :(得分:0)

查看您的测试,看来您的service.AddEmployee做得太多了:)。您可以拆分验证逻辑等。

考虑这样的测试(引导你的设计)。这使用FakeDbSet

[Test]
public void Should_add_any_valid_employees_and_save_them()
{
   //arrange
   var validator = new Mock<IEmployeeValidator>();
   validator.Setup(v => v.Validate(It.IsAny<Employee>())).Returns(true);

   // ... setup the context and the dbset

   var service = new MyService(validator.Object, mockContext.Object)

   var newData = new List<EmployeeDto>
      {
         new EmployeeDto{Id = 1},
         new EmployeeDto{Id = 2}
      }

   // act
   service.AddEmployees(newData);

   // assert
   mockContext.Verify(c => c.SaveChanges(), Times.Once());
   Assert.True(fakeDbSet.Count == newData.Count);
   CollectionAssert.AreEquivalent( newData.Select(e=>e.Id), mockData.Select(e=>e.Id));
}

[Test]
public void Should_not_add_any_invalid_employees()
{
   //arrange
   var validator = new Mock<IEmployeeValidator>();
   validator.Setup(v => v.Validate(It.IsAny<Employee>())).Returns(false);

   // ... setup the context and the dbset

   var service = new MyService(validator.Object, mockContext.Object)

   var newData = new List<EmployeeDto>
      {
         new EmployeeDto{Id = 1},
         new EmployeeDto{Id = 2}
      }

   // act
   service.AddEmployees(newData);

   // assert
   mockContext.Verify(c => c.SaveChanges(), Times.Never());
   Assert.True(fakeDbSet.Count == 0);
   CollectionAssert.IsEmpty( mockData );
}

你也可以使用混合IEmployeeDtoToEmployee映射器,所以你也抽象了这部分功能。