我最近接受了采访,要求我制作传统的FizzBuzz解决方案:
输出1到100之间的数字列表。
- 对于3和5的所有倍数,该数字将替换为“FizzBuzz”
- 对于所有剩余的3的倍数,该数字将替换为“Fizz”
- 对于所有剩余的5的倍数,该数字将替换为“Buzz”
我的解决方案是用Java编写的,但这不是必需的。面试官很想看到一些TDD的证据,所以本着这种精神,我开始制作我自己的本土FizzBuzz单元测试:
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友好”的答案:
变更:
将FizzBuzz逻辑与输出生成分开,使解决方案更具可扩展性
每次测试只需一个断言,以简化它们
仅测试每种情况下最基本的逻辑单元
确认字符串构建的最终测试也已经过验证
代码:
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());
}
}
答案 0 :(得分:6)
TDD与XP和敏捷哲学密切相关的原因是有道理的。它驱使我们使用可测试代码的小单元。因此,像TheSimplestThingWhichCouldPossiblyWork或单一责任原则这样的概念不属于测试驱动的方法。
在您的方案中显然没有发生过这种情况。你注意的是数字数组,而不是FizzBuzz位(线索确实存在于问题中)。
显然你处于完全人为的状态,并且很难伪造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);
}
关键在于每个产生清晰的结果,并且很容易从失败的测试开始。
完成FizzBuzz位后,可以轻松完成数组操作。关键是要避免硬编码。最初我们可能不想要一个完整的实现:生成相对少量的元素就足够了,比如15。这样做的好处是可以产生更好的设计。毕竟,如果面试官回来说“实际上我想要一个121个元素的阵列”,你需要改变多少代码?有多少次测试?
TDD的挑战之一是知道从哪里开始。 Gojko Adzic写了一篇发人深思的文章,描述a Coding Dojo implementing a game of Go。
“是否有机会暴露我的翻译方法会有标记 以后封装的理由反对我?“
TDD中最激烈争论的话题之一。可能的答案是:
没有正确的答案,通常取决于具体要求或个人心血来潮。例如,虽然FizzBuzz本身很简单,但我们经常需要编写代码来获取数据,应用业务规则并返回验证结果。有时,规则需要应用于单个数据项,有时针对整个记录集,有时针对任何一个。
因此,暴露这两种方法的API不一定是错误的。当然,在面试的情况下,它让您有机会讨论API设计的细微差别,这是一个很好的对话话题。
答案 1 :(得分:2)
FizzBuzz难题有两个部分:循环,并为给定的int生成正确的字符串。传统上,人们将两者结合成一个功能(这非常合理,因为它非常简单),但对于TDD,我会考虑第二部分,以便您可以独立测试它。在伪代码中:
String[] fizzbuzz(int count)
for i: 0 ... count:
line = fizzOrBuzz(i)
output.add(line)
现在,您可以在不必循环的情况下测试fizzOrBuzz
方法,并且确信它有效,然后您可以测试循环。确保找到可能的边缘情况(0,-1,Integer.MAX_VALUE
)。
对于像FizzBuzz这样简单的东西,我会把它限制在那里:我不会创建一个模拟的FizzBuzzer等等。但要准备好捍卫这个决定(基本上说,功能的简单性并不能保证一个非常复杂的测试)。当我采访人们时,我想建议一个不太好的反例来表明他们的想法,看看他们是否可以捍卫他们的想法(或者可能改进它!)。
答案 2 :(得分:1)
我不会要求TDD的丰富经验,所以请不要认为我是一个权威人士!考虑到这一点,这是我的0.02美元:
FizzBuzz
实例。generate()
并将该代码放入getResultAsArray()
。 (非常轻微。)你提到的其他可能的改变对我来说似乎有些过分。
还有一点:FizzBuzz? Eugh!这是一个非常糟糕的示例问题,因为它是如此微不足道......