TDD:在重构代码的同时打破所有现有的测试用例 [英] TDD : Breaks all the existing test cases while refactoring the code

查看:25
本文介绍了TDD:在重构代码的同时打破所有现有的测试用例的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已经开始在我的项目中遵循 TDD.但是自从我开始,即使看了一些文章,我也很困惑,因为发展变慢了.每当我重构我的代码时,我都需要更改我之前编写的现有测试用例,否则它们就会开始失败.

I have started following TDD in my project. But ever since I started, even after reading some articles, I am confused since the development has slowed down. Whenever I refactor my code, I need to change the existing test cases I have written before because otherwise they will start failing.

以下是我最近重构的一个类的示例:

The following is an example of a class I recently refactored:

public class SalaryManager
{
    public string CalculateSalaryAndSendMessage(int daysWorked, int monthlySalary)
    {
        int salary = 0, tempSalary = 0;
        if (daysWorked < 15)
        {
            tempSalary = (monthlySalary / 30) * daysWorked;
            salary = tempSalary - 0.1 * tempSalary;
        }
        else
        {
            tempSalary = (monthlySalary / 30) * daysWorked;
            salary = tempSalary + 0.1 * tempSalary;
        }

        string message = string.Empty;
        if (salary < (monthlySalary / 30))
        {
            message = "Salary cannot be generated. It should be greater than 1 day salary.";
        }
        else
        {
            message = "Salary generated as per the policy.";
        }

        return message;
    }
}

但现在我用一种方法做很多事情,所以为了遵循单一职责原则 (SRP),我将其重构为如下所示:

But now I am doing lot of things in one method, so to follow the Single Responsibility Principle (SRP), I refactored it to something like below:


public class SalaryManager
{
    private readonly ISalaryCalculator _salaryCalculator;        
    private readonly SalaryMessageFormatter _messageFormatter;
    public SalaryManager(ISalaryCalculator salaryCalculator, ISalaryMessageFormatter _messageFormatter){
        _salaryCalculator = salaryCalculator;
        _messageFormatter = messageFormatter;
    }

    public string CalculateSalaryAndSendMessage(int daysWorked, int monthlySalary)
    {
        int salary = _salaryCalculator.CalculateSalary(daysWorked, monthlySalary);
        string message = _messageFormatter.FormatSalaryCalculationMessage(salary);

        return message;
    }
}

public class SalaryCalculator
{
    public int CalculateSalary(int daysWorked, int monthlySalary)
    {
        int salary = 0, tempSalary = 0;
        if (daysWorked < 15)
        {
            tempSalary = (monthlySalary / 30) * daysWorked;
            salary = tempSalary - 0.1 * tempSalary;
        }
        else
        {
            tempSalary = (monthlySalary / 30) * daysWorked;
            salary = tempSalary + 0.1 * tempSalary;
        }
        return salary;
    }
}

public class SalaryMessageFormatter
{
    public string FormatSalaryCalculationMessage(int salary)
    {
        string message = string.Empty;
        if (salary < (monthlySalary / 30))
        {
            message = "Salary cannot be generated. It should be greater than 1 day salary.";
        }
        else
        {
            message = "Salary generated as per the policy.";
        }
        return message;
    }
}

这可能不是最好的例子.但主要的一点是,一旦我进行了重构,我为 SalaryManager 编写的现有测试用例就开始失败,我不得不使用模拟来修复它们.

This may not be the greatest of examples. But the main point is that as soon as I did the refactoring, my existing test cases which I wrote for the SalaryManager started failing and I had to fix them using mocking.

这种情况在读取时间场景中一直发生,开发时间也随之增加.我不确定我是否以正确的方式进行 TDD.请帮我理解.

This happens all the time in read time scenarios, and the time of development increases with it. I am not sure if I am doing TDD in the right way. Please help me to understand.

推荐答案

这个问题发生在重构改变现有单元的职责时,尤其是通过引入新单元或移除现有单元.

This problem happens when refactoring changes responsibilities of existing units especially by introducing new units or removing existing units.

您可以采用 TDD 风格执行此操作,但您需要:

You can do this in TDD style but you need to:

  1. 做一些小步骤(这排除了同时提取两个类的更改)
  2. 重构(这也包括重构测试代码!)

起点

在你的情况下(我使用更抽象的类似 python 的语法来减少样板,这个问题与语言无关):

Starting point

In your case you have (I use more abstract python-like syntax to have less boilerplate, this problem is language independent):

class SalaryManager:
    def CalculateSalaryAndSendMessage(daysWorked, monthlySalary):
      // code that has two responsibilities calculation and formatting

你有测试类.如果你没有测试,你需要先创建这些测试(在这里你可以找到 有效地使用遗留代码 真的很有帮助)或者在许多情况下与一些重构一起能够重构你的代码甚至更多(重构是改变代码结构而不改变其功能,所以你需要进行测试以确保您不会更改功能).

You have test class for it. If you don't have tests you need to create these tests first (here you may find Working Effectively with Legacy Code really helpful) or in many cases together with some refactoring to be able to refactor you code even more (refactoring is changing code structure without changing its functionality so you need to have test to be sure you don't change the functionality).

class SalaryManagerTest:
    def test_calculation_1():
      // some test for calculation

    def test_calculation_2():
      // another test for calculation

    def test_formatting_1():
      // some test for formatting

    def test_formatting_2():
      // another test for calculation

    def test_that_checks_both_formatting_and_calculation():
      // some test for both

将计算提取到一个类中

现在让您了解如何将计算责任提取到类中.

Extracting calculation to a class

Now let's you what to extract calculation responsibility to a class.

您无需更改SalaryManager 的API 即可立即完成.在经典的 TDD 中,您分小步进行,并在每一步之后运行测试,如下所示:

You can do it right away without changing API of the SalaryManager. In classical TDD you do it in small steps and run tests after each step, something like this:

  1. 将计算提取到 SalaryManager
  2. 的函数(比如 calculateSalary)
  3. 创建空的SalaryCalculator
  4. SalaryManager
  5. 中创建SalaryCalculator类的实例
  6. 移动 calculateSalarySalaryCalculator
  1. extract calculation to a function (say calculateSalary) of SalaryManager
  2. create empty SalaryCalculator class
  3. create instance of SalaryCalculator class in SalaryManager
  4. move calculateSalary to SalaryCalculator

有时(如果 SalaryCalculator 很简单,并且它与 SalaryManager 的交互很简单)您可以停在这里,根本不更改测试.所以计算测试仍然是 SalaryManager 的一部分.随着SalaryCalculator 复杂性的增加,通过SalaryManager 对其进行测试将变得困难/不切实际,因此您需要进行第二步 - 重构测试.

Sometimes (if SalaryCalculator is simple and its interactions with SalaryManager are simple) you can stop here and do not change tests at all. So tests for calculation will still be part of SalaryManager. With the increasing of complexity of SalaryCalculator it will be hard/impractical to test it via SalaryManager so you will need to do the second step - refactor tests as well.

我会做这样的事情:

  1. 基本上通过复制类将SalaryManagerTest分成SalaryManagerTestSalaryCalculatorTest
  2. SalaryManagerTest
  3. 中删除 test_calculation_1test_calculation_1
  4. SalaryCalculatorTest中只留下test_calculation_1test_calculation_1
  1. split SalaryManagerTest into SalaryManagerTest and SalaryCalculatorTest basically by copying the class
  2. remove test_calculation_1 and test_calculation_1 from SalaryManagerTest
  3. leave only test_calculation_1 and test_calculation_1 in SalaryCalculatorTest

现在在 SalaryCalculatorTest 中测试计算功能,但通过 SalaryManager 进行.你需要做两件事:

Now tests in SalaryCalculatorTest test functionality for calculation but do it via SalaryManager. You need to do two things:

  1. 确保你有集成测试来检查计算是否发生
  2. 更改 SalaryCalculatorTest 使其不使用 SalaryManager
  1. make sure you have integration test that checks that calculation happens at all
  2. change SalaryCalculatorTest so that it does not use SalaryManager

集成测试

  1. 如果您还没有这样的测试(test_that_checks_both_formatting_and_calculation 可能是这样的测试),请创建一个测试,当涉及到 SalaryManager 的计算时,它会执行一些简单的用例.立>
  2. 如果您愿意,您可能希望将该测试移至 SalaryManagerIntegrationTest
  1. If you don't have such test already (test_that_checks_both_formatting_and_calculation may be such a test) create a test that does some simple usecase when calculation is involved from SalaryManager
  2. You may want to move that test to SalaryManagerIntegrationTest if you wish

使 SalaryCalculatorTest 使用 SalaryCalculator

SalaryCalculatorTest 中的测试都是关于计算的,所以即使他们与经理打交道,他们的本质和重要部分是为计算提供输入,然后检查它的结果.

Make SalaryCalculatorTest use SalaryCalculator

Tests in SalaryCalculatorTest are all about calculation so even if they deal with manager their essence and important part is providing input to calculation and then check the result of it.

现在我们的目标是以某种方式重构测试,以便可以轻松地为计算器切换管理器.

Now our goal is to refactor the tests in a way so that it is easy to switch manager for calculator.

计算测试可能如下所示:

The test for calculation may look like this:

class SalaryCalculatorTest:

    def test_short_period_calculation(self):
       manager = new SalaryManager()
       DAYS_WORKED = 1
       result = manager.CalculateSalaryAndSendMessage(DAYS_WORKED, SALARY)
       assertEquals(result.contains('Salary cannot be generated'), True)

这里有三件事:

  1. 准备测试对象
  2. 调用动作
  3. 检查结果

请注意,此类测试将以某种方式检查计算结果.它可能令人困惑和脆弱,但它会以某种方式做到这一点.因为应该有一些外部可见的方式来区分计算是如何结束的.否则(如果它没有任何可见的效果)这样的计算是没有意义的.

Note that such test will check outcome of the calculation in some way. It may be confusing and fragile but it will do it somehow. As there should be some externally visible way to distinguish how calculation ended. Otherwise (if it does not have any visible effect) such calculation does not make sense.

你可以这样重构:

  1. manager的创建提取到一个函数createCalculator中(可以这样调用,因为从测试角度创建的对象是计算器)
  2. rename manager -> sut(被测系统)
  3. manager.CalculateSalaryAndSendMessage 调用提取到函数 `calculate(calculator, days,salary)
  4. 将支票提取到一个函数中assertPeriodIsTooShort(result)
  1. extract creation of the manager to a function createCalculator (it is ok to call it this way as the object that is created from the test perspective is the calculator)
  2. rename manager -> sut (system under test)
  3. extract manager.CalculateSalaryAndSendMessage invocation into a function `calculate(calculator, days, salary)
  4. extract the check into a function assertPeriodIsTooShort(result)

现在测试没有直接引用经理,它反映了测试内容的本质.

Now the test has no direct reference to manager, it reflects the essence of what is tested.

应该对这个测试类中的所有测试和函数进行这样的重构.不要错过重用其中一些的机会,例如 createCalculator.

Such refactoring should be done with all tests and functions in this test class. Don't miss the opportunity to reuse some of them like createCalculator.

现在您可以更改在 createCalculator 中创建的对象以及在 assertPeriodIsTooShort 中预期的对象(以及如何完成检查).这里的诀窍是仍然控制这种变化的大小.如果它太大(也就是说,在经典 TDD 的几分钟内更改后您无法使测试变为绿色),您可能需要创建 createCalculatorassert... 并首先在一个测试中使用它们,然后在其他测试中逐渐用旧的替换它们.

Now you can change what object is created in createCalculator and what object is expected (and how the check is done) in assertPeriodIsTooShort. The trick here is to still control the size of that change. If it is too big (that is you can't make test green after the change in couple minutes in classical TDD) you may need to create a copy of the createCalculator and assert... and use them in one test only first but then gradually replace old with one in other tests.

这篇关于TDD:在重构代码的同时打破所有现有的测试用例的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆