如何改进我的FizzBu​​zz解决方案以获得TDD角色?

时间:2012-03-06 12:51:38

标签: java tdd fizzbuzz

我最近接受了采访,要求我制作传统的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());
    }
}

3 个答案:

答案 0 :(得分: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写了一篇发人深思的文章,描述a Coding Dojo implementing a game of Go


  

“是否有机会暴露我的翻译方法会有标记   以后封装的理由反对我?“

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

  1. 将方法保密,并将单元测试嵌入到类中。
  2. 针对公共方法编写测试,然后将测试通过的方法设为私有,并重构测试。
  3. 上述变化:使用条件编译(或类似)来公开或隐藏方法。
  4. 让他们公开
  5. 没有正确的答案,通常取决于具体要求或个人心血来潮。例如,虽然FizzBu​​zz本身很简单,但我们经常需要编写代码来获取数据,应用业务规则并返回验证结果。有时,规则需要应用于单个数据项,有时针对整个记录集,有时针对任何一个。

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

答案 1 :(得分: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​​zzer等等。但要准备好捍卫这个决定(基本上说,功能的简单性并不能保证一个非常复杂的测试)。当我采访人们时,我想建议一个不太好的反例来表明他们的想法,看看他们是否可以捍卫他们的想法(或者可能改进它!)。

答案 2 :(得分:1)

我不会要求TDD的丰富经验,所以请不要认为我是一个权威人士!考虑到这一点,这是我的0.02美元:

  1. 我会创建所有这些静态方法实例方法,然后为每个测试创建一个新的FizzBuzz实例。
  2. 摆脱generate()并将该代码放入getResultAsArray()。 (非常轻微。)
  3. 在单元测试类中使用常量很好。 (即@APC说的话)。
  4. 你提到的其他可能的改变对我来说似乎有些过分。

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