如何在TDD中重构测试?

3

我正在进行这个TDD练习:http://osherove.com/tdd-kata-1

我写了以下代码(这个练习的1-5步骤,我已经为它写了单元测试):

public class StringCalculator
{
    private readonly string[] _defaultSeparators = { ",", "\n" };

    public int Add(string numbers)
    {
        // Parser section (string to list of ints)
        var separators = _defaultSeparators;

        var isSeparatorDefinitionSpecified = numbers.StartsWith("//");
        if (isSeparatorDefinitionSpecified)
        {
            var endOfSeparatorDefinition = numbers.IndexOf('\n');

            var separator = numbers.Substring(2, endOfSeparatorDefinition - 2);

            numbers = numbers.Substring(endOfSeparatorDefinition);
            separators = new[] { separator };
        }

        var numbersArray = numbers.Split(separators, StringSplitOptions.RemoveEmptyEntries);
        var numbersArrayAsInts = numbersArray.Select(int.Parse).ToArray();

        // Validator section
        var negativeNumbers = numbersArrayAsInts.Where(c => c < 0).ToArray();
        if (negativeNumbers.Any())
        {
            throw new Exception(string.Format("negatives not allowed ({0})", string.Join(", ", negativeNumbers)));
        }

        return numbersArrayAsInts.Sum();
    }
}

现在我想将代码重构为以下内容:
public int Add(string numbers)
{
    var numbersAsInts = CalculatorNumbersParser.Parse(numbers);

    CalculatorNumbersValidator.Validate(numbersAsInts);

    return numbersAsInts.Sum();
}

我应该如何计划重构我的代码和单元测试呢?

我认为,我应该将部分测试移动到新创建的实现类测试中(CalculatorNumbersParserTests和CalculatorNumbersValidatorTests),更改一些现有测试,并添加针对Parse和Validate方法执行的测试。

但是,在不破坏测试的情况下,正确的做法是什么呢?


只需将代码移动到内部类中,通过StringCalculator(public)类型的测试间接测试。或者,两者也可以实现为同一类型中的私有方法。 - Gishu
2个回答

4
我建议不要将测试移动,因为这样做会使测试与实现绑定,这意味着它们非常脆弱,所以每次更改实现时都必须更改测试。当您拥有大量代码库时,这可能很快变得昂贵,并且可能成为阻止进行更改的因素。
您现有的测试应该指定字符串计算器的行为,因此只要保持所需的行为,就可以重构实现。
我倾向于将单元视为“行为单元”,可能需要几个类来实现这一点。
如果您将某些类放在不同的程序集中,则可能会发生变化,在这种情况下,您可能需要在新程序集旁边进行一些新测试,以确保这些组件的行为不会意外更改,但我怀疑您不会这样做。
如果您开始在多个地方重用类,则可能需要单独的测试来指定类的行为,而不考虑它们在这些地方的使用。

你的意思是在我的测试中实例化 CalculatorNumbersParser 和 CalculatorNumbersValidator(而不是模拟)?当我的单元测试变成集成测试时,在 TDD 中这样做可以吗? - czesio
这取决于你认为什么是“单元”。我认为“单元”是指功能或行为的单元,可能涉及多个类。如果按照你所建议的(对每个类进行独立测试),根据我的经验,你将拥有非常脆弱的测试,需要花费大量精力在每次更改代码时进行修改。在《单元测试的艺术》一书中,定义为“一个工作单元可以跨越单个方法、整个类或多个协同工作的类,以实现一个可验证的单一逻辑目标。” - Sam Holder
@czesio:或者你可以将这个功能移动到私有方法或私有类中。你需要考虑它们(这些类)是否存在于计算器类之外是有意义的;也就是说,它们代表独立的功能。对于这个kata练习来说,这样的重构可能过于牵强。 - k.m

3
我认为@Sam Holder已经涵盖了大部分内容。我要补充的一点是,当您重构代码时,应将您创建的任何不编写特定测试的类标记为internal(假设您使用.net),以便它们不会在所包含的程序集之外可见。
我倾向于认为public类应该得到单独测试,因为其他引用您程序集的代码可以轻松实例化这些类。而另一方面,internal类可以被看作是实现细节,并且通常可以通过程序集的公共接口进行测试。当然,这还取决于您所做的事情/代码的复杂性等,但这是我的一般规则。

这是一个很好的通用规则,你可以使用 InternalsVisibleTo 属性来确保你的测试仍然可以看到那些类(如果必要的话,尽管不应该,但有时候...)。 - Sam Holder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接