方法调用转移的单元测试策略是什么?

时间:2020-03-06 14:25:32  来源:igfitidea点击:

我有以下情况:

public class CarManager
{
  ..

  public long AddCar(Car car)
  {
      try
      {
         string username = _authorizationManager.GetUsername();
         ...
         long id = _carAccessor.AddCar(username, car.Id, car.Name, ....);
         if(id == 0)
         {
             throw new Exception("Car was not added");
         }
         return id;
      } catch (Exception ex) {
         throw new AddCarException(ex);
      }
  }

  public List AddCars(List cars)
  {
     List ids = new List();
     foreach(Car car in cars)
     {
         ids.Add(AddCar(car));
     }
     return ids;
  }
}

我正在嘲笑_reportAccessor,_authorizationManager等。

现在,我要对CarManager类进行单元测试。
我应该对AddCar()进行多个测试,例如

AddCarTest()
AddCarTestAuthorizationManagerException()
AddCarTestCarAccessorNoId()
AddCarTestCarAccessorException()

对于AddCars(),我应该重复所有以前的测试,因为AddCars()调用AddCar()好像是在重复自己吗?我可能不应该从AddCars()调用AddCar()吗? <p />

请帮忙。

解决方案

单元测试应仅专注于其对应的测试类。不属于同一类型的所有类属性都应被模拟。

假设我们有一个使用某种数据访问对象(例如CarPlatesDAO)的类(CarRegistry),该对象从Relational数据库加载/存储车牌号。

在测试CarRegistry时,我们不必在乎CarPlateDAO是否正常运行。由于我们的DAO具有它自己的单元测试。

我们只需创建行为类似于DAO的模拟,并根据预期的行为返回正确或者错误的值。我们可以将此模拟DAO插入CarRegistry,并仅测试目标类而不关心所有聚合的类是否都是"绿色"。

模拟可分离可测试的类,并更好地专注于特定功能。

这里有两个问题:

  • 一次单元测试应该做的不仅仅是测试方法。它们的设计应证明与系统的其余部分集成时,班级可以完成其设计的工作。因此,我们应该模拟依赖项,然后针对实际使用类的每种方式编写测试。对于我们编写的每个(非平凡的)类,都会有一些场景,这些场景涉及以特定模式调用客户端代码的方法。
  • AddCars调用AddCar并没有错。我们应该重复测试以进行错误处理,但前提是要达到目的。单元测试的非正式规则之一是"测试到无聊的程度"或者(就像我想的那样)"测试直到恐惧消失"。否则,我们将永远写测试。因此,如果我们确信测试不会增加任何价值,则不要编写。当然,我们可能错了,在这种情况下,我们可以稍后再添加它。我们不必在第一轮就进行一次完美的测试,而只是一个坚实的基础,我们可以在此基础上更好地了解自己的课程需要做的。

对AddCar类进行单元测试时,请创建将使用每个代码路径的测试。如果_authorizationManager.GetUsername()可以引发异常,请创建一个测试,将在该测试中模拟该对象。顺便说一句:不要抛出或者捕获Exception的实例,而是派生一个有意义的Exception类。

对于AddCars方法,我们绝对应该调用AddCar。但是我们可以考虑将AddCar虚拟化,并对其进行覆盖,以测试列表中的所有汽车都被调用了。

有时,我们必须更改类设计以提高可测试性。

编写探索方法中每种可能情况的测试是一种好习惯。这就是我在项目中进行单元测试的方式。诸如" AddCarTestAuthorizationManagerException()"," AddCarTestCarAccessorNoId()"或者" AddCarTestCarAccessorException()"之类的测试使我们思考代码失败的所有不同方式,这导致我发现了一种我可能会错过的方法的新型失败以及改善课程的整体设计。

在类似AddCars()调用AddCar()的情况下,我将模拟AddCar()方法并计算AddCars()调用它的次数。我使用的模拟库允许我创建CarManager的模拟,仅模拟AddCar()方法,而不模拟AddCars()。然后,单元测试可以设置期望调用" AddCar()"的次数,我们可以从传入的汽车列表的大小中知道该次数。

Should I have multiple tests for
  AddCar() such as
  
  AddCarTest()
  AddCarTestAuthorizationManagerException()
  AddCarTestCarAccessorNoId()
  AddCarTestCarAccessorException()

绝对地!这告诉我们有价值的信息

For AddCars() should I repeat all previous tests as AddCars() calls AddCar() - it seems
  like repeating oneself? Should I perhaps not be calling AddCar() from AddCars()?

从AddCars调用AddCar是一个好主意,它避免违反DRY原理。同样,我们应该重复测试。以这种方式思考一下,我们已经为AddCar编写了测试,因此在测试AddCard时,我们可以假定AddCar确实做到了。

让我们这样假设,假设AddCar在另一个类中。我们将不了解授权管理器。在不了解AddCar必须做什么的情况下测试AddCars。

对于AddCars,我们需要测试所有正常边界条件(做一个空列表工作,等等)。我们可能不需要测试AddCar引发异常的情况,因为我们没有尝试在AddCars中捕获它。