如何对重构为策略模式的功能进行单元测试?


10

如果我的代码中有一个像这样的函数:

class Employee{

    public string calculateTax(string name, int salary)
    {
        switch (name)
        {
            case "Chris":
                doSomething($salary);
            case "David":
                doSomethingDifferent($salary);
            case "Scott":
               doOtherThing($salary);               
       }
}

通常,我会使用工厂类和策略模式将其重构为使用多态性:

public string calculateTax(string name)
{
    InameHandler nameHandler = NameHandlerFactory::getHandler(name);
    nameHandler->calculateTax($salary);
}

现在,如果我正在使用TDD,那么calculateTax()在重构之前,我将对原始版本进行一些测试。

例如:

calculateTax_givenChrisSalaryBelowThreshold_Expect111(){}    
calculateTax_givenChrisSalaryAboveThreshold_Expect111(){}

calculateTax_givenDavidSalaryBelowThreshold_Expect222(){}   
calculateTax_givenDavidSalaryAboveThreshold_Expect222(){} 

calculateTax_givenScottSalaryBelowThreshold_Expect333(){}
calculateTax_givenScottSalaryAboveThreshold_Expect333(){}

重构后,我将具有Factory类NameHandlerFactory和的至少3个实现InameHandler

我应该如何重构我的测试?我应该claculateTax()从中删除单元测试,EmployeeTests并为的每个实现创建一个Test类InameHandler吗?

我也应该测试工厂课程吗?

Answers:


6

旧的测试恰好可以验证calculateTax仍然可以正常工作。但是,您不需要太多的测试用例,只需3个(或者如果您还想使用意外的值来测试错误处理,则可能需要更多个name)。

每个单独的案例(目前在doSomething等人中实现)也必须具有自己的一组测试,这些测试用于测试与每个实现相关的内部细节和特殊案例。在新的设置中,可以/应该将这些测试转换为相应策略类的直接测试。

我更喜欢仅在它们执行的代码及其实现的功能完全不存在时才删除旧的单元测试。否则,编码到这些测试中的知识仍然有意义,只有测试本身需要重构。

更新资料

在的测试calculateTax(称为高级测试)和针对各个计算策略的测试低级测试)之间可能会有一些重复-这取决于您的实现。

我猜您测试的原始实现会断言特定税收计算的结果,从而隐含地验证了特定税收策略是用来产生税收的。如果保留此架构,则确实会有重复。但是,正如@Kristof所暗示的那样,您也可以使用模拟来实施高级测试,以仅验证是否已选择并调用了正确的(模拟)策略calculateTax。在这种情况下,高水平测试和低水平测试之间不会重复。

因此,如果重构受影响的测试不太昂贵,我会选择后一种方法。但是,在现实生活中,当进行一些大规模重构时,如果可以为我节省足够的时间,我可以容忍少量测试代码重复:-)

我也应该测试工厂课程吗?

同样,这取决于。注意测试calculateTax有效地测试了工厂。因此,如果工厂代码switch像上面的代码一样是琐碎的块,那么这些测试可能就是您所需要的。但是,如果工厂做了一些更棘手的事情,则可能要专门进行一些测试。一切都归结为您需要多少测试才能确信所讨论的代码确实有效。如果在阅读代码(或分析代码覆盖率数据)时看到未测试的执行路径,请专门进行一些测试以执行这些测试。然后重复此步骤,直到您对代码完全自信为止。


我对代码进行了一些修改,以使其更接近实际的实际代码。现在salary,向该函数calculateTax()添加了第二个输入。这样,我想我将为原始功能和策略类的3个实现复制测试代码。
Songo 2012年

@Songo,请参阅我的更新。
彼得Török

5

首先,我不是TDD或单元测试的专家,但是这是测试方法(我将使用类似伪代码的代码):

CalculateTaxDelegatesToNameHandler()
{
    INameHandlerFactory fakeNameHandlerFactory = Fake(INameHandlerFactory);
    INameHandler fakeNameHandler = Fake(INameHandler);

    A.Call.To(fakeNameHandlerFactory.getHandler("John")).Returns(fakeNameHandler);

    Employee employee = new Employee(fakeNameHandlerFactory);
    employee.CalculateTax("John");

    Assert.That.WasCalled(fakeNameHandler.calculateTax());
}

因此,我将测试calculateTax()雇员类的方法是否正确地要求其NameHandlerFactory为a NameHandler,然后调用calculateTax()返回的方法NameHandler


嗯,所以你的意思是我应该将测试改为行为测试(测试某些函数已被调用)并在委托类上进行值声明?
Songo 2012年

是的,那就是我要做的。我确实会为NameHandlerFactory和NameHandler编写单独的测试。如果有这些功能,则没有理由在该Employee.calculateTax()方法中再次测试其功能。这样,当您引入新的NameHandler时,您无需添加额外的Employee测试。
克里斯托夫·克拉斯

3

您要上一堂课(负责所有工作的员工),并进行三组课:工厂,员工(仅包含策略)和策略。

因此,请进行3组测试:

  1. 隔离测试工厂。它是否正确处理输入。当您传递未知信息时会发生什么?
  2. 孤立地测试员工。您可以设置任意策略,并且按预期工作吗?如果没有策略或工厂设定,会发生什么?(如果在代码中可行)
  3. 孤立地测试策略。每个人都执行您期望的策略吗?他们是否以一致的方式处理奇数边界输入?

您当然可以对整个Shebang进行自动测试,但是现在这些测试更像是集成测试,应该这样对待。


2

在编写任何代码之前,我将从测试工厂开始。模拟我需要的东西,我会强迫自己考虑一下实现和用例。

比我将实现一个Factory并继续对每个实现进行测试,最后对这些测试实现自己。

最后,我将删除旧的测试。


2

我的意见是您什么都不做,这意味着您不应添加任何新测试。

我强调这是一种观点,它实际上取决于您对对象期望的感知方式。您认为该类别的用户是否愿意提供一种计税策略?如果他不在乎,那么测试应该反映出这一点,而单元测试反映出的行为应该是,他们应该不在乎该类已经开始使用策略对象来计算税款。

使用TDD时,我实际上遇到了几次此问题。我认为主要原因是策略对象不是自然依赖性,而不是说诸如外部资源(文件,数据库,远程服务等)这样的体系结构边界依赖性。由于这不是自然的依赖关系,因此我通常不会根据这种策略来决定班级的行为。我的直觉是,只有在班级期望改变的情况下,我才应该更改考试。

Bob叔叔有一篇很棒的文章,在使用TDD时恰好谈到了这个问题。

我认为测试每个单独的类的趋势正在杀死TDD。TDD的全部优点在于,您可以使用测试来刺激设计方案,而不是相反。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.