问题描述
限时送ChatGPT账号..我已经开始在我的项目中遵循 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:
做一些小步骤(这排除了同时提取两个类的更改)重构(这也包括重构测试代码!)起点
在你的情况下(我使用更抽象的类似 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:
SalaryManager
的函数(比如 calculateSalary
)创建空的SalaryCalculator
类在SalaryManager
中创建SalaryCalculator
类的实例移动 calculateSalary
到 SalaryCalculator
extract calculation to a function (say calculateSalary
) of SalaryManager
create empty SalaryCalculator
class
create instance of SalaryCalculator
class in SalaryManager
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.
我会做这样的事情:
基本上通过复制类将SalaryManagerTest
分成SalaryManagerTest
和SalaryCalculatorTest
从 SalaryManagerTest
中删除 test_calculation_1
和 test_calculation_1
在SalaryCalculatorTest
中只留下test_calculation_1
和test_calculation_1
split SalaryManagerTest
into SalaryManagerTest
and SalaryCalculatorTest
basically by copying the class
remove test_calculation_1
and test_calculation_1
from SalaryManagerTest
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:
SalaryCalculatorTest
使其不使用 SalaryManager
make sure you have integration test that checks that calculation happens at all
change SalaryCalculatorTest
so that it does not use SalaryManager
集成测试
如果您还没有这样的测试(test_that_checks_both_formatting_and_calculation
可能是这样的测试),请创建一个测试,当涉及到 SalaryManager
的计算时,它会执行一些简单的用例.立>如果您愿意,您可能希望将该测试移至 SalaryManagerIntegrationTest
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
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)
这里有三件事:
准备测试对象调用动作检查结果请注意,此类测试将以某种方式检查计算结果.它可能令人困惑和脆弱,但它会以某种方式做到这一点.因为应该有一些外部可见的方式来区分计算是如何结束的.否则(如果它没有任何可见的效果)这样的计算是没有意义的.
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.
你可以这样重构:
将manager
的创建提取到一个函数createCalculator
中(可以这样调用,因为从测试角度创建的对象是计算器)rename manager
-> sut
(被测系统)将 manager.CalculateSalaryAndSendMessage
调用提取到函数 `calculate(calculator, days,salary)将支票提取到一个函数中assertPeriodIsTooShort(result)
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)
rename manager
-> sut
(system under test)
extract manager.CalculateSalaryAndSendMessage
invocation into a function `calculate(calculator, days, salary)
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 的几分钟内更改后您无法使测试变为绿色),您可能需要创建 createCalculator
和 assert...
并首先在一个测试中使用它们,然后在其他测试中逐渐用旧的替换它们.
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屋!
更多推荐
[db:关键词]
发布评论