我的公司正在进行单元测试,我在重构服务层代码方面遇到了一些麻烦。这是我写的一些代码的例子:
public class InvoiceCalculator:IInvoiceCalculator
{
public CalculateInvoice(Invoice invoice)
{
foreach (InvoiceLine il in invoice.Lines)
{
UpdateLine(il);
}
//do a ton of other stuff here
}
private UpdateLine(InvoiceLine line)
{
line.Amount = line.Qty * line.Rate;
//do a bunch of other stuff, including calls to other private methods
}
}
在这个简化的情况下(它从1,000行的类中减少了1个公共方法和~30个私有方法),我的老板说我应该能够单独测试我的CalculateInvoice和UpdateLine(UpdateLine实际上调用了3个其他私有方法) ,并执行数据库调用)。但是我该怎么做呢?他建议的重构对我来说似乎有点令人费解:
//Tiny part of original code
public class InvoiceCalculator:IInvoiceCalculator
{
public ILineUpdater _lineUpdater;
public InvoiceCalculator (ILineUpdater lineUpdater)
{
_lineUpdater = lineUpdater;
}
public CalculateInvoice(Invoice invoice)
{
foreach (InvoiceLine il in invoice.Lines)
{
_lineUpdater.UpdateLine(il);
}
//do a ton of other stuff here
}
}
public class LineUpdater:ILineUpdater
{
public UpdateLine(InvoiceLine line)
{
line.Amount = line.Qty * line.Rate;
//do a bunch of other stuff
}
}
我可以看到依赖关系现在如何被破坏,我可以测试这两个部分,但是这也会从我的原始类中创建20-30个额外的类。我们只在一个地方计算发票,所以这些碎片实际上不可重复使用。这是进行此更改的正确方法,还是您建议我做一些不同的事情?
谢谢!
尔杰斯
答案 0 :(得分:5)
这是Feature Envy的一个例子:
line.Amount = line.Qty * line.Rate;
应该看起来更像:
var amount = line.CalculateAmount();
许多小课程没有任何问题,这与重新使用性无关,而与适应性有关。如果您有许多单一责任类,则可以更轻松地查看系统的行为,并在需求发生变化时进行更改。大班级有责任,这使得改革变得非常困难。
答案 1 :(得分:1)
IMO这一切都取决于UpdateLine()方法的“重要性”。如果它只是一个实现细节(例如,它可以很容易地在CalculateInvoice()方法中内联,并且它们只会损害可读性),那么您可能不需要单独对主类进行单元测试。
另一方面,如果UpdateLine()方法对业务逻辑有一些价值,如果您可以想象当您需要独立于类的其余部分更改此方法(并因此单独测试)时的情况,那么你应该继续将它重构为一个单独的LineUpdater类。
你可能不会以这种方式结束20-30个类,因为大多数私有方法实际上只是实现细节,不值得单独测试。
答案 2 :(得分:1)
嗯,你的老板在单元测试方面更正确:
他现在可以在不测试UpdateLine()函数的情况下测试CalculateInvoice()。他可以传递模拟对象而不是真正的LineUpdater对象,只测试CalculateInvoice(),而不是一大堆代码。
这样对吗?这取决于。你的老板想要进行真正的单元测试。第一个例子中的测试不是单元测试,而是集成测试。
集成测试前单元测试的优点是什么?
1)单元测试允许您只测试一种方法或属性,而不受其他方法/数据库等的影响
2)第二个优点 - 单元测试执行得更快(例如,你说UpdateLine使用数据库)因为它们不测试所有嵌套方法。嵌套方法可以是数据库调用,因此如果您有数千个测试,您的测试可能会运行缓慢(几分钟)
3)第三个优点:如果您的方法进行数据库调用,那么有时您需要设置数据库(用测试所需的数据填充它)并且它可能并不容易 - 也许您将不得不编写几页代码为测试准备数据库。使用单元测试,您可以将数据库调用与正在测试的方法分开(使用模拟对象)。
但是!我并不是说单位测试更好。他们只是不同。正如我所说,单元测试允许您快速隔离地测试一个单元。集成测试更容易,并允许您测试不同方法和层的联合工作的结果。老实说,我更喜欢集成测试:)
另外,我有几点建议:
1)我不认为Amount字段是个好主意。似乎Amount字段是额外的,因为它的值可以基于其他2个公共字段来计算。无论如何你想要这样做,我会把它做为只读属性,它返回Qty * Rate
2)通常,拥有一个由1000行组成的类可能意味着它的设计很糟糕并且应该重构。
现在,我希望你能更好地了解情况并做出决定。此外,如果你了解情况,你可以与老板谈谈,你可以一起决定。
答案 3 :(得分:0)
米歇尔
答案 4 :(得分:0)
你老板的榜样对我来说很合理。
在设计任何场景时,我会考虑的一些关键注意事项是:
单一责任原则
一个班级只应因某种原因而改变。
每个新类是否证明其存在
是否仅为了创建类,或者它们是否封装了有意义的逻辑部分?
您是否能够单独测试每段代码?
在你的场景中,只看名字,看起来你正在徘徊于单一责任 - 你有一个IInvoiceCalculator,但这个类还负责更新InvoiceLines。您不仅要让测试更新行为变得非常困难,而且当计算业务规则在更新规则发生变化时更改和时,您现在需要更改InvoiceCalculator类。
然后有关于更新逻辑的问题 - 逻辑是否证明了单独的类?这真的取决于并且很难说没有看到代码,但事实上你的老板想要测试的逻辑会表明它不仅仅是一个简单的线上调用数据层。
你说这个重构会创建大量额外的类,(我认为你的意思是跨所有业务实体,因为我在你的例子中只看到了几个新类及其接口)但是你必须考虑一下你从中获得了什么。看起来您可以获得代码的完全可测试性,能够独立引入新计算和新更新逻辑,以及更清晰地封装什么是单独的业务逻辑。
上面的收益当然需要进行成本效益分析,但由于你的嘘声是要求它们,听起来他很高兴他们会得到回报,反对额外的工作实施这样的代码。
关于单独测试的最后一点,也是你老板设计方式的一个关键优势 - 你的公共方法越接近实际执行的代码,就越容易注入存根或模拟对于未经测试的系统部分。例如,如果您正在测试一个调用数据层的更新方法,那么您不想测试数据层,因此通常会注入一个模拟器。如果您需要首先通过所有计算器逻辑传递该模拟数据层,那么您的测试设置将变得更加复杂,因为模拟现在需要满足许多其他潜在需求,与实际测试无关。
虽然这种方法最初是额外的工作,但我会说大部分工作是时候考虑设计,之后,在你加快注入基于注入的代码风格之后,以这种方式构建的软件的原始实现时间实际上是可比较的。
答案 5 :(得分:0)
你的hoss'方法是依赖注入的一个很好的例子,如何让你使用模拟ILineUpdater
来有效地进行测试。