我已经开始在我的项目中使用TDD了。但是自从我在阅读了一些文章后开始阅读,我感到有点困惑,因为开发速度已经放缓。每当我重构代码时,我都需要更改之前编写的现有测试用例,因为它们会开始失败。 以下是示例:
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原则我将它重构为如下所示:
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编写的现有测试用例开始失败,我不得不使用模拟来修复它们。
这在读取时间场景中始终发生,并且开发时间随之增加。我不确定我是否正在编写TDD的写入方式。请帮助理解。
答案 0 :(得分:0)
每当我重构代码时,我都需要更改之前编写的现有测试用例,因为它们会开始失败。
这肯定表明出现了问题。重构的流行定义类似于this
REFACTORING是一种严格的技术,用于重构现有的代码体系,改变其内部结构而不改变其外部行为。
进行单元测试的部分原因是单元测试正在评估实现的外部行为。失败的单元测试表明实现更改以某种方式改变了外部可观察行为。
在这种特殊情况下,您似乎更改了API - 具体而言,您删除了默认构造函数,该构造函数已成为创建SalaryManager
实例的API的一部分;这不是“重构”,而是一个向后突破的变化。
在重构时引入新的协作者没有任何问题,但是你应该以不违反当前API合同的方式这样做。
public class SalaryManager
{
public SalaryManager(ISalaryCalculator salaryCalculator, ISalaryMessageFormatter _messageFormatter){
_salaryCalculator = salaryCalculator;
_messageFormatter = messageFormatter;
}
public SalaryManager() {
this(new SalaryCalculator(), new SalaryMessageFormatter())
}
其中SalaryCalculator
和SalaryMessageFormatter
应该是产生与您原来相同的可观察行为的实现。
当然,在某些情况下,我们需要引入向后突破性的变化。但是,“重构”不适合该案例。在许多情况下,您可以在几个阶段中实现所需的结果:首先使用新测试扩展API(重构以删除与现有实现的重复),然后删除评估旧API的测试,最后删除旧API。
答案 1 :(得分:0)
当重构更改现有单位的职责时,特别是通过引入新单位或删除现有单位,就会出现此问题。
您可以使用TDD样式执行此操作,但需要:
在你的情况下,你有(我使用更抽象的python类语法来减少样板,这个问题与语言无关):
class SalaryManager:
def CalculateSalaryAndSendMessage(daysWorked, monthlySalary):
// code that has two responsibilities calculation and formatting
你有测试类。如果你没有测试,你需要先创建这些测试(这里你可能会发现Working Effectively with Legacy Code非常有帮助),或者在很多情况下还有一些重构,以便能够重构代码甚至更多(重构正在改变)代码结构而不改变其功能,因此您需要进行测试以确保您不会更改功能)。
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
现在让您了解如何将计算责任提取到班级。
您可以在不更改SalaryManager
的API的情况下立即执行此操作。在经典TDD中,您可以在很小的步骤中完成并在每个步骤之后运行测试,如下所示:
calculateSalary
SalaryManager
)
SalaryCalculator
类SalaryCalculator
SalaryManager
类的实例
calculateSalary
移至SalaryCalculator
有时(如果SalaryCalculator
很简单并且与SalaryManager
的交互很简单),您可以在此处停止并且根本不更改测试。因此,计算测试仍将是SalaryManager
的一部分。随着SalaryCalculator
复杂性的增加,通过SalaryManager
测试它将是困难/不切实际的,所以你需要做第二步 - 重构测试。
我会做这样的事情:
SalaryManagerTest
拆分为SalaryManagerTest
和SalaryCalculatorTest
test_calculation_1
test_calculation_1
和SalaryManagerTest
test_calculation_1
test_calculation_1
和SalaryCalculatorTest
醇>
现在测试SalaryCalculatorTest
测试功能以进行计算,但是通过SalaryManager
进行测试。你需要做两件事:
SalaryCalculatorTest
,以便它不会使用SalaryManager
test_that_checks_both_formatting_and_calculation
可能是这样的测试),请在SalaryManager
SalaryManagerIntegrationTest
SalaryCalculatorTest
中的测试都是关于计算的,所以即使他们处理经理他们的本质,重要的部分是为计算提供输入,然后检查结果。
现在我们的目标是以某种方式重构测试,以便很容易将管理员换成计算器。
计算测试可能如下所示:
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)
这里有三件事:
请注意,此类测试将以某种方式检查计算结果。它可能会令人困惑和脆弱,但它会以某种方式做到这一点。因为应该有一些外部可见的方式来区分计算结束的方式。否则(如果它没有任何可见效果)这样的计算没有意义。
你可以像这样重构:
manager
的创建提取到函数createCalculator
(可以这样调用,因为从测试角度创建的对象是计算器)manager
- &gt; sut
(被测系统)manager.CalculateSalaryAndSendMessage
调用提取到函数`calculate(calculator,days,salary)assertPeriodIsTooShort(result)
现在测试没有直接引用管理器,它反映了测试内容的本质。
这种重构应该使用此测试类中的所有测试和函数来完成。不要错过重用其中一些内容的机会,例如createCalculator
。
现在,您可以在createCalculator
中更改assertPeriodIsTooShort
中创建的对象以及预期的对象(以及检查的完成方式)。这里的诀窍是仍然控制变化的大小。如果它太大(即在经典TDD中几分钟后更改后无法进行绿色测试),您可能需要创建createCalculator
和assert...
的副本并使用它们在一次测试中只先进行一次测试,然后在其他测试中逐渐替换旧测试。
答案 2 :(得分:0)
您的困惑是由于对单元测试的普遍误解。在某些时候,人们开始散布这样的神话,即一个单元就像一个单一的类甚至一个单一的方法。这是由相关文献、文章和博客文章推动的,这些文章和博客文章展示了如何使用非常小的孤立类来完成。人们忽略了这样做的原因是,您无法在具有现实生活应用程序的书籍或文章中展示开发过程,因为它对于格式来说太大了。
确实,需要隔离单元测试。这并不意味着“单元”需要完全隔离。测试本身需要与其他测试隔离。这意味着测试应该相互依赖,并且应该可以单独甚至并行运行它们。
单元测试的相关单元应该是一个用例。用例是您的应用程序的一个功能。在博客引擎中,“创建博客文章”是一个用例。在酒店预订系统中,“查找空闲房间”是一个用例。通常,您会在所谓的应用程序服务中找到这些入口点。这就是您应该瞄准的粒度。嘲笑丑陋的外部依赖项,如数据库、文件系统和外部服务。如果您重构应用程序的内部结构,则测试不会中断,因为您不会更改用例的行为。想要拆分或合并您域中的类?用例将是稳定的。更改域对象之间的交互方式。测试保持绿色。