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

本文关键字:测试数据 方法 单元测试 | 更新日期: 2023-09-27 18:32:47

假设我使用最小起订量对实体框架 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的属性,以及每个Position将拥有的List<Employee>。 在 ValidEmployees 中拥有Clients = {ValidClients[0],ValidClients[1]并在 ValidClient 中拥有SalesRepresentative = ValidEmployees[0]不是很好吗?

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

 Assert.AreEqual
 (
     TestData.ValidEmployees[i],
     MockSet.Object.Find(TestData.ValidEmployees[i].EmployeeID
 ) 

如果 ValidEmployees 中没有导航数据,仍然返回 true?这是否意味着我应该找到另一种确保状态的方法?

无论如何,这些是我遇到的问题。 我只是设置了完全错误的单元测试吗? 我应该如何获得健壮、独立、干燥和准确的单元测试? 我在这里错过了什么?

任何帮助都是值得赞赏的,即使这意味着以不同的心态从头开始。 这是我的第一个项目,我非常认真地对待测试,但我觉得进展得并不顺利。 因此,对不起文字墙。 有时我觉得我没有问正确的问题来达到我想去的地方。

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

回答问题的第二部分(关于测试数据(。我不会在测试中使用测试数据类,这将使测试变得脆弱,并可能引入微妙的错误,因为对测试数据类的更改可能会影响不同测试类中的许多不相关的测试,谷歌对象母亲反模式。

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

例:

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

查看您的测试,您的服务似乎。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映射器,所以你也可以抽象这部分功能。