2010-01-24 52 views
7

我的公司在单元测试中,我在重构服务层代码时遇到了一些问题。下面是一些代码,我写了一个例子:重构服务层类

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分公共法和〜30倍私立学校的1000线类下降),我的老板说我应该能够分别测试我的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额外的课程。我们只在一个地方计算发票,因此这些部分不会真正可重复使用。这是做出这个改变的正确方法,还是你建议我做一些不同的事情?

谢谢!

Jess

回答

1

IMO这一切都取决于 '显著' 那UpdateLine()方法确实是。如果它只是一个实现细节(例如,它可以很容易地在CalculateInvoice()方法内部进行内联,并且它们只会损害可读性),那么您可能不需要单独对主类进行单独测试。另一方面,如果UpdateLine()方法对业务逻辑具有一定的价值,如果您可以想象当您需要独立于其余类别更改此方法的情况(并因此单独对其进行测试),那么可以使用UpdateLine()那么你应该继续重构它到一个单独的LineUpdater类。

你可能不会以这种方式得到20-30个类,因为大多数这些私有方法实际上只是实现细节,并不值得单独测试。

0

是的,很好的一个。我不确定InvoiceLine对象是否也包含一些逻辑,否则,您可能还需要一个IInvoiceLine。 我有时会遇到同样的问题。一方面,你希望做正确的事情并且单元测试你的代码,但是当涉及数据库调用和可能的文件写入时,它会导致大量额外的工作来设置第一个测试,所有测试对象在文件写入和数据库io涉及发生,接口,断言,并且您还想测试数据层是否包含任何错误。因此,更“过程”和“单位”的测试通常更容易构建。 如果你有一个项目会被改变很多(将来)和这个代码的很多依赖项(也许其他程序读取文件或数据库数据),那么对你的所有部分进行稳定的单元测试可能会很好代码,投资时间是值得的。 但是如果这个项目像我最近的客户那样说'让我们活下去吧,也许我们会在明年和明年调整一下会有新的东西',比我自己不会那么努力去得到所有人单元测试正在运行。

米歇尔

1

那么,你的老板在单元测试方面更加正确:
他现在能够在不测试UpdateLine()函数的情况下测试CalculateInvoice()。他可以传递模拟对象而不是真正的LineUpdater对象,并只测试CalculateInvoice(),而不是一大堆代码。
是不是?这取决于。你的老板想要进行真正的单元测试。在你的第一个例子中测试不是单元测试,而是集成测试。

集成测试之前单元测试的优点是什么?
1)单元测试允许您只测试一个方法或属性,而不受其他方法/数据库等的影响。
2)第二个优点 - 单元测试执行速度更快(例如,您说UpdateLine使用数据库),因为它们不测试所有嵌套的方法。嵌套方法可以是数据库调用,所以如果你有数千次测试,你的测试可能会运行缓慢(几分钟)。
3)第三个好处:如果你的方法进行数据库调用,那么有时你需要设置数据库(填充测试所需的数据),这可能不容易 - 也许你必须编写几页代码只是为了准备测试数据库。使用单元测试,可以将数据库调用与正在测试的方法分开(使用模拟对象)。

但是!我并不是说单元测试更好。他们只是不同。正如我所说的,单元测试允许您快速隔离地测试一个单元。集成测试更容易,并允许您测试不同方法和图层的联合工作结果。老实说,我更喜欢集成测试:)

另外,我有几个建议给你:
1)我不认为有金额字段是一个好主意。看起来“金额”字段是额外的,因为它的值可以根据其他2个公共字段进行计算。如果你想这样做,我会把它作为只读属性返回Qty * Rate。
2)通常,拥有一个由1000行组成的类可能意味着它设计得不好,应该重构。

现在,我希望你更好地了解情况,并可以决定。此外,如果你了解情况,你可以与你的老板交谈,你可以一起决定。

0

你老板的例子对我来说看起来很合理。

一些我尝试设计的任何方案时要记住的关键因素是:

  1. 单一职责原则

    类应该只改变的原因之一。

  2. 是否每个新类证明它的存在

    已创建只是为了它的缘故类,或者说它们封装逻辑的一个有意义的部分?

  3. 你能够独立测试每段代码吗?

在您的方案,只是看着名字,这样看来,你是漂泊单一职责了 - 你有一个IInvoiceCalculator,但这个类是则还负责更新InvoiceLines。您不仅使测试更新行为变得非常困难,而且当计算业务规则更改时,您需要更改InvoiceCalculator类。

然后有关于更新逻辑的问题 - 逻辑证明一个单独的类是否合理?这真的很有依赖性,而且很难说没有看到代码,但肯定是你的老板想要被测逻辑的事实表明,它不仅仅是一个简单的在班轮上呼叫数据层。

你说这个重构会创建大量额外的类(我在所有业务实体中都采用这种方式,因为我只在您的示例中看到了几个新类和它们的接口),但是您有考虑你从中得到什么。看起来您可以获得完整的代码测试能力,能够独立引入新的计算和新的更新逻辑,并且可以更清晰地封装单独的业务逻辑。

增益当然要进行成本效益分析,但是由于您的嘘声正在寻求它们,因此听起来他很高兴他们会付出代价,反对通过这种方式执行代码的额外工作。

关于独立测试的最后第三点也是您的老板设计的一个关键好处 - 公共方法越接近执行实际工作的代码,注入桩或嘲笑系统中未经测试的部分。例如,如果您正在测试关闭数据层的更新方法,则不需要测试数据层,因此您通常会注入一个模拟。如果您需要首先通过所有计算器逻辑传递模拟数据层,那么您的测试设置将变得复杂得多,因为模拟现在需要满足许多其他潜在需求,而与有关的实际测试无关。

虽然这种方法最初是额外的工作,但我认为大部分工作是考虑设计的时候,在此之后,以及在掌握更多基于注入的代码风格之后,以这种方式构建的软件的原始实施时间实际上是可比较的。

5

这是Feature Envy一个例子:

line.Amount = line.Qty * line.Rate; 

应该看起来可能更像:

var amount = line.CalculateAmount(); 

没有什么毛病很多小的类,它不是关于重用性就像它的适应性一样。如果您有许多单一职责类别,则可以更轻松地查看系统的行为,并在需求更改时更改它。大班有相互缠绕的责任,这使得很难改变。

+0

我们所有的实体对象基本都是DTO,所以所有的业务逻辑都在服务类中。但是在发票行计算器中有很多其他逻辑,我只是​​展示了一件。 – 2010-01-24 21:19:19

0

你hoss的方法是依赖注入的一个很好的例子,以及如何这样做可以让你使用模拟ILineUpdater有效地进行测试。