8

我最近接受了一次采访,被要求制作传统的 FizzBu​​zz 解决方案:

输出从 1 到 100 的数字列表。

  • 对于所有 3 和 5 的倍数,数字都替换为“FizzBu​​zz”
  • 对于所有剩余的 3 的倍数,该数字将替换为“Fizz”
  • 对于所有剩余的 5 的倍数,该数字将替换为“Buzz”

由于角色的原因,我的解决方案是用 Java 编写的,但这不是必需的。面试官很想看到 TDD 的一些证据,所以本着这种精神,我开始制作我自己的本土 FizzBu​​zz 单元测试:

public class FizzBuzzTest {

    @Test
    public void testReturnsAnArrayOfOneHundred() {
        String[] result = FizzBuzz.getResultAsArray();
        assertEquals(100, result.length);
    }

    @Test
    public void testPrintsAStringRepresentationOfTheArray() {
        String result = FizzBuzz.getResultAsString();
        assertNotNull(result);
        assertNotSame(0, result.length());
        assertEquals("1, 2", result.substring(0, 4));
    }

    @Test
    public void testMultiplesOfThreeAndFivePrintFizzBuzz() {
        String[] result = FizzBuzz.getResultAsArray();

        // Check all instances of "FizzBuzz" in array
        for (int i = 1; i <= 100; i++) {
            if ((i % 3) == 0 && (i % 5) == 0) {
                assertEquals("FizzBuzz", result[i - 1]);
            }
        }
    }

    @Test
    public void testMultiplesOfThreeOnlyPrintFizz() {
        String[] result = FizzBuzz.getResultAsArray();

        // Check all instances of "Fizz" in array
        for (int i = 1; i <= 100; i++) {
            if ((i % 3) == 0 && !((i % 5) == 0)) {
                assertEquals("Fizz", result[i - 1]);
            }
        }
    }

    @Test
    public void testMultiplesOfFiveOnlyPrintBuzz() {
        String[] result = FizzBuzz.getResultAsArray();

        // Check all instances of "Buzz" in array
        for (int i = 1; i <= 100; i++) {
            if ((i % 5) == 0 && !((i % 3) == 0)) {
                assertEquals("Buzz", result[i - 1]);
            }
        }
    }
}

我最终的实现变成了:

public class FizzBuzz {

    private static final int MIN_VALUE = 1;
    private static final int MAX_VALUE = 100;


    private static String[] generate() {
        List<String> items = new ArrayList<String>();

        for (int i = MIN_VALUE; i <= MAX_VALUE; i++) {

            boolean multipleOfThree = ((i % 3) == 0);
            boolean multipleOfFive = ((i % 5) == 0);

            if (multipleOfThree && multipleOfFive) {
                items.add("FizzBuzz");
            }
            else if (multipleOfThree) {
                items.add("Fizz");
            }
            else if (multipleOfFive) {
                items.add("Buzz");
            }
            else {
                items.add(String.valueOf(i));
            }
        }

        return items.toArray(new String[0]);
    }

    public static String[] getResultAsArray() {
        return generate();
    }

    public static String getResultAsString() {
        String[] result = generate();
        String output = "";
        if (result.length > 0) {
            output = Arrays.toString(result);
            // Strip out the brackets from the result
            output = output.substring(1, output.length() - 1);
        }
        return output;
    }

    public static final void main(String[] args) {
        System.out.println(getResultAsString());
    }
}

整个解决方案花了我大约 20 分钟的时间晚了一天晚上,包括在提交之前紧张地检查我的代码比必要的时间长得多:)

回顾我最初提交的内容: 早期我决定将我的“倍数”计算合并到 generate() 方法中以避免过度设计,我现在认为这是一个错误;此外,单独的 getResultAsArray/generate 方法显然是 OTT。getResultAsString 也可以与 main() 方法合并,因为一个只是委托给另一个。

我对 TDD 仍然相当缺乏经验,我觉得在这种情况下这可能让我失望。我正在寻找可以改进这种方法的其他方法,尤其是在 TDD 实践方面?


更新

基于以下非常有用的建议,我重新编写了我现在认为更“对 TDD 友好”的答案:

变化:

  • 将 FizzBu​​zz 逻辑与输出生成分离,使解决方案更具可扩展性

  • 每个测试只有一个断言,以简化它们

  • 在每种情况下只测试最基本的逻辑单元

  • 还验证了确认字符串构建的最终测试

编码:

public class FizzBuzzTest {

    @Test
    public void testMultipleOfThreeAndFivePrintsFizzBuzz() {
        assertEquals("FizzBuzz", FizzBuzz.getResult(15));
    }

    @Test
    public void testMultipleOfThreeOnlyPrintsFizz() {
        assertEquals("Fizz", FizzBuzz.getResult(93));
    }

    @Test
    public void testMultipleOfFiveOnlyPrintsBuzz() {
        assertEquals("Buzz", FizzBuzz.getResult(10));
    }

    @Test
    public void testInputOfEightPrintsTheNumber() {
        assertEquals("8", FizzBuzz.getResult(8));
    }

    @Test
    public void testOutputOfProgramIsANonEmptyString() {
        String out = FizzBuzz.buildOutput();
        assertNotNull(out);
        assertNotSame(0, out.length());
    }
}

public class FizzBuzz {

    private static final int MIN_VALUE = 1;
    private static final int MAX_VALUE = 100;

    public static String getResult(int input) {
        boolean multipleOfThree = ((input % 3) == 0);
        boolean multipleOfFive = ((input % 5) == 0);

        if (multipleOfThree && multipleOfFive) {
            return "FizzBuzz";
        }
        else if (multipleOfThree) {
            return "Fizz";
        }
        else if (multipleOfFive) {
            return "Buzz";
        }
        return String.valueOf(input);
    }

    public static String buildOutput() {
        StringBuilder output = new StringBuilder();

        for (int i = MIN_VALUE; i <= MAX_VALUE; i++) {
            output.append(getResult(i));

            if (i < MAX_VALUE) {
                output.append(", ");
            }
        }

        return output.toString();
    }

    public static final void main(String[] args) {
        System.out.println(buildOutput());
    }
}
4

3 回答 3

6

TDD 与 XP 和敏捷哲学密切相关是有原因的。它驱使我们使用小单元的可测试代码。因此,像 TheSimplestThingWhichCouldPossiblyWork 或单一职责原则这样的概念不属于测试驱动的方法。

在您的场景中显然没有发生这种情况。您专注于数字数组,而不是 FizzBu​​zz 位(线索确实在问题中)。

显然,您处于完全人为的情况下,很难伪造 TDD。但我希望“真正的”TDD 代码能够公开翻译方法。这是这样的:

@Test     
public void testOtherNumber() {        
     String result = FizzBuzz.translateNumber(23);
     assertEquals("23", result);
 } 

@Test     
public void testMultipleOfThree() {        
     String result = FizzBuzz.translateNumber(3);
     assertEquals("Fizz", result);
 } 

@Test     
public void testMultipleOfFive() {        
     String result = FizzBuzz.translateNumber(25);
     assertEquals("Buzz", result);
 } 

@Test     
public void testMultipleOfFifteen() {        
     String result = FizzBuzz.translateNumber(45);
     assertEquals("FizzBuzz", result);
 } 

关键是这些中的每一个都会产生明确的结果,并且很容易从失败的测试开始。

完成 FizzBu​​zz 位之后,做数组的东西就很容易了。关键是要避免硬编码。最初我们可能不想要一个完整的实现:生成相对少数的元素就足够了,比如 15 个。这具有产生更好设计的优势。毕竟,如果面试官回来说“其实我想要一个包含 121 个元素的数组”,你需要修改多少代码?多少测试?


TDD 的挑战之一是知道从哪里开始。Gojko Adzic 就此写了一篇发人深省的文章,描述了一个实现围棋游戏的 Coding Dojo


“有没有机会暴露我的翻译方法以后会以封装为由对我不利?”

TDD 中争论最激烈的话题之一。可能的答案是:

  1. 保持方法私有并将单元测试嵌入到类中。
  2. 针对公共方法编写测试,然后将测试通过的方法设为私有,然后重构测试。
  3. 上述变化:使用条件编译(或类似的)来公开或隐藏方法。
  4. 让他们公开

没有正确的答案,通常取决于具体要求或个人突发奇想。例如,虽然 FizzBu​​zz 本身很简单,但我们经常需要编写获取数据、应用业务规则并返回验证结果的代码。有时该规则需要应用于单个数据项,有时针对整个记录集,有时针对任何一个。

因此,公开这两种方法的 API 不一定是错误的。当然,在面试的情况下,它让你有机会讨论 API 设计的细微差别,这是一个很好的话题。

于 2012-03-06T13:19:19.343 回答
2

FizzBu​​zz 难题有两个部分:循环和为给定的 int 生成正确的字符串。传统上,人们将两者结合为一个函数(这是完全合理的,因为它非常简单),但对于 TDD,我会考虑第二部分,以便您可以独立测试它。在伪代码中:

String[] fizzbuzz(int count)
    for i: 0 ... count:
        line = fizzOrBuzz(i)
        output.add(line)

现在,您fizzOrBuzz无需继续循环即可测试该方法,并且确信它有效,然后您可以测试该循环。确保你遇到了可能的边缘情况(0、-1、Integer.MAX_VALUE)。

对于像 FizzBu​​zz 这样简单的东西,我会限制它:我不会创建一个模拟的 FizzBu​​zz 之类的东西。但是要准备好为这个决定辩护(基本上是说函数的简单性并不保证非常复杂的测试)。当我采访人们时,我喜欢为他们的想法提出一个不太好的反例,看看他们是否可以捍卫自己的想法(或者可能改进它!)。

于 2012-03-06T13:35:11.900 回答
1

我不会声称有丰富的 TDD 经验,所以请不要认为我是在作为权威发言!考虑到这一点,这是我的 0.02 美元:

  1. 我会创建所有这些静态方法的实例方法,然后FizzBuzz为每个测试创建一个新实例。
  2. 摆脱generate()并将该代码放入getResultAsArray(). (这个非常小。)
  3. 在你的单元测试类中有常量是很好的。(即@APC 所说的)。

你提到的其他可能的变化对我来说似乎有点矫枉过正。

还有一点:FizzBu​​zz?哎呀!这是一个非常糟糕的示例问题,因为它是如此微不足道......

于 2012-03-06T13:32:37.897 回答