在TDD重构之后编写更多的单元测试

本文关键字:单元测试 TDD 重构 之后 | 更新日期: 2023-09-27 18:22:28

这是我们反复讨论的问题,人们对此的看法似乎大相径庭。

在进行TDD时,最基本的问题是,是否应该在周期的重构步骤之后添加额外的单元测试。我说的不是开始下一个周期的下一个测试,而是覆盖由于重构而发生的任何更改的测试。

这可能最好用一个现实生活中的例子来解释。在TDD周期的绿色之后,我们有以下代码:

    public bool ShouldVerifyDomain
    {
        get
        {
            return this.Orders.SelectMany(x => x.Items).Any(x => x.Product.RequiresDomainVerification);
        }
    }

现在,我看到这个,想,嗯,linq语句可能会更整洁,更容易阅读,不会太违反Demeter,让我们重构它。所以我在Order上创建了以下内容:

 public bool HasItemsThatRequireDomainVerification
 {
     get
     {
          return this.Items.Any(x => x.Product.RequiresCascadeDomainVerification);
     }
 }

并将ShouldVerifyDomain修改为:

  public bool ShouldVerifyDomain
  {
      get
      {
           return this.Orders.Any(x => x.HasItemsThatRequireDomainVerification);
      }
  }

好吧,看起来好多了,我更高兴。让我们继续我列表中的下一个测试。。。但是等等,我们现在正在通过另一个对象上的属性测试属性HasItemsThatRequireDomainVerification。。。。是一个真正的单元测试,还是应该添加一个测试来直接测试HasItemsThatRequireDomainVerification

我的感觉?我认为这不会增加多少价值。我认为这会增加套房的维护负担,需要时间,而且在未来做出改变时不会给我们更多的信心。

它能给我们什么?Order公共接口的"文档"。

想法?

在TDD重构之后编写更多的单元测试

您的重构步骤是否添加或更改了功能?如果是这样,那么这是一个无效的重构步骤。您应该放弃这些更改,并首先为新功能添加一个测试。

然而,在你的例子中,我不认为情况一定如此。你所做的一切都与提取方法非常相似。您将现有逻辑合并到另一个位置,并从现有位置调用该逻辑。现有的测试仍然在测试这一点。

重构之后,如果您担心需要添加更多的测试,那么首先应该查看测试覆盖率。如果你仍然是100%(或者和重构前一样接近),那么你可能仍然很好。另一方面,如果重构添加了测试未涵盖的代码路径,那么您可以选择:

  • 您的代码是否需要这些代码路径?如果是这样的话,测试是不够的。您应该退出重构,为新代码路径添加失败的测试,然后添加新代码路径
  • 如果您的代码不需要这些代码路径,那么它们为什么存在?把他们赶走

你所问的问题与一个关于测试覆盖率的古老问题非常相似,这个问题以多种形式被问到:

  • 我应该测试私人会员吗
  • 我应该为每种方法写一个单独的测试吗
  • 每个对象的每个成员都应该进行测试吗

和所有事情一样,答案总是"取决于情况"。所有的代码都应该测试,但每一行代码都不需要自己的测试。例如,假设我在类上有一个属性:

public class Customer
{
    public string Name { get; set; }
}

我是否需要编写一个测试来实例化Customer,向其写入Name值,然后断言它可以读取相同的值?显然不是。如果失败了,那么就出现了严重的错误。但是,这一行代码是否应该包含在测试中?绝对地某个地方应该有一个使用CustomerName的测试。如果没有,如果系统中没有测试使用此属性,则测试不完整,或者系统实际上不需要此属性,应将其删除。

换句话说,在编写测试时,您并没有真正测试代码。您正在测试系统的功能。实现该功能的代码与测试是分开的,并且与测试并行。两人不需要了解对方的很多细节。如果某个东西的外部可见功能发生了变化,测试应该改变以匹配(并验证)它。如果外部可见功能没有改变,测试也不应该改变。他们仍然应该验证相同的功能。

当你进行TDD时,你的测试应该处于功能"功能测试"的级别,所以只要功能没有改变,你就不应该改变你的测试。

只要功能的输入和输出相同,更改实现或重构在TDD中被视为细节。

TDD不应该让您拥有100%的覆盖率。

另一方面,如果您使用单元测试作为代码解释,或者希望拥有100%的覆盖率(我们在这里谈论的是单元测试,因为它们应该只针对一段代码),那么每当实现适应于覆盖所有情况时,这些单元测试都应该更改,但这不是TDD的目标。

您没有更改行为,只更改了语法。代码仍然以相同的方式工作,只是编写方式不同而已。我认为,只要它仍然以同样的方式工作,您的单元测试仍然是可靠的。

我认为,如果重构要求我们开始测试重构后的新代码,那么我们最终会陷入兔子洞。它什么时候结束?