8

I had an interview recently where I was asked to produce the traditional FizzBuzz solution:

Output a list of numbers from 1 to 100.

  • For all multiples of 3 and 5, the number is replaced with "FizzBuzz"
  • For all remaining multiples of 3, the number is replaced with "Fizz"
  • For all remaining multiples of 5, the number is replaced with "Buzz"

My solution was written in Java because of the role, but this was not a requirement. The interviewer was keen to see some evidence of TDD, so in that spirit I went about producing my very own home-grown FizzBuzz unit test:

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]);
            }
        }
    }
}

My resulting implementation became:

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());
    }
}

The whole solution took me around 20 minutes late one evening, including nervously checking over my code for a lot longer than necessary before submitting it :)

Reviewing what I originally submitted: Early on I decided to merge my "multiple of" calculation into the generate() method to avoid over-engineering, which I now think was a mistake; also, the separate getResultAsArray/generate methods were clearly OTT. The getResultAsString could also be merged with the main() method, since one just delegates to the other.

I'm still fairly inexperienced with TDD and I feel this may have let me down in this case. I'm looking for other ways I might have improved on this approach, particularly with regard to TDD practices?


Update

Based on the very useful suggestions below, I've reworked my answer to something I now consider would have been more "TDD-friendly":

Changes:

  • Separated the FizzBuzz logic from the output generation to make the solution more scalable

  • Just one assertion per test, to simplify them

  • Only testing the most basic unit of logic in each case

  • A final test to confirm the string building is also verified

The code:

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());
    }
}
seanhodges
  • 17,426
  • 15
  • 71
  • 93
  • 5
    You may also want to try http://codereview.stackexchange.com/ – Peter Brown Mar 06 '12 at 13:10
  • Oh, it looks like you can't move it to codereview.stackexchange.com, I'll cross-reference it instead. – seanhodges Mar 06 '12 at 13:27
  • I've already received some great answers here, but just in case I've cross-referenced the question on CodeReview to expose it to the audience there: http://codereview.stackexchange.com/questions/9749/ways-to-improve-my-coding-test-fizzbuzz-solution-for-a-tdd-role – seanhodges Mar 06 '12 at 13:59

3 Answers3

6

There's a reason why TDD is strongly linked with XP and Agile philosophies. It drives us to small units of testable code. So concepts like TheSimplestThingWhichCouldPossiblyWork or the Single Responsibility Principle fall out of a test driven approach.

That clearly hasn't happened in your scenario. You fixated on the array of numbers, rather than the FizzBuzz bit (the clue really is in the question).

Obviously you're in a totally artificial situation and it's hard to fake TDD. But I would expect "real" TDD code to have exposed the translation methods. Something this:

@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);
 } 

The point being that each of those produces a clear result and is easy to start with a failing test.

Having done the FizzBuzz bit it is a cinch to do the array stuff. The key point about that is to avoid the hardcoding. Initially we might not want a full implementation: it would be sufficient to generate a relative handful of number of elements, say 15. This has the advantage of producing a better design. After all, if the interviewer came back and said "Actually I want an array of 121 elements", how much of your code would you have to change? How many tests?


One of the challenges of TDD is knowing where to start. Gojko Adzic wrote a thought-provoking piece on this, describing a Coding Dojo implementing a game of Go.


"Is there a chance exposing my translation methods would have marked against me on the grounds of encapsulation later on?"

One of the most hotly debated topic in TDD. Possible answers are:

  1. keep the methods private and embed the unit tests in the class.
  2. write the tests against public methods, then make the methods private one the tests pass, and refactor the tests.
  3. variation on the above: use conditional compilation (or similar) to expose or hide the methods.
  4. just keep them public

There are no right answers, and often it will depend upon specific requirements or personal whim. For instance, while FizzBuzz itself is trivial we are quite often required to write code which takes data, appplies a business rule and returns a validation outcome. Sometimes the rule needs to be applied to a single item of data, sometimes against entire record sets and sometimes against either.

So an API which exposes both methods is not necessarily wrong. And certainly in an interview situation it gives you the opportunity to discuss the nuances of API design, which is a good topic of conversation.

APC
  • 144,005
  • 19
  • 170
  • 281
  • also one of the test names `testReturnsAnArrayOfOneHundred` would need to be changed (which would break historical test results); and what if the interviewer came back and said "actually I now want to input an array instead of generating one within the method" – Seph Mar 06 '12 at 13:28
  • I see your point. Probably a better approach would have been to put the logic for calculating a single entry into a separate public method; then testing that instead of a whole array of results. I was focusing too much on ensuring the original requirements were met (e.g. ensure list is 1 to 100). – seanhodges Mar 06 '12 at 13:32
  • @Seph perhaps having separate getResultAsArray and getResultAsString was not too far off the mark in that case? If I allowed getResultAsString accept an array as an input I would have met that requirement... – seanhodges Mar 06 '12 at 13:35
  • @Seph Not sure I agree about inputting an array - I thought there was a guideline in TDD to do "just enough to pass your tests". – vaughandroid Mar 06 '12 at 13:43
  • Some very useful points in this answer - thanks APC. As you suggest, I'm still a budding TDD practitioner and it seems clear many of my mistakes were pretty fundamental. I'll spend some more time practising and read some more on TDD before accepting an interview like that again. It is definitely a methodology I would like to be more adept at. – seanhodges Mar 06 '12 at 13:56
  • Is there a chance exposing my translation methods would have marked against me on the grounds of encapsulation later on? Or is this just an expected trade-off in TDD? In other words: the question only expected an array from the solution, not a single value, would translateNumber() be unacceptable exposure of logic? – seanhodges Mar 06 '12 at 14:14
  • @Baqueta I meant, if the question was changed to "I now want to take an array as input, how much code would you have to change?" interviewers love to change the scope of work entirely just to see how you cope with major scope change, often the care more about that than your original answer. – Seph Mar 07 '12 at 06:16
2

There are two parts to the FizzBuzz puzzle: the loop, and generating the right string for a given int. Traditionally, people combine the two into one function (which is perfectly reasonable, since it's so simple), but for TDD, I would factor out the second part so you can test it independently. In pseudocode:

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

Now, you can test the fizzOrBuzz method without having to go on the loop, and, confident that it works, you can then test the loop. Make sure you hit possible edge cases (0, -1, Integer.MAX_VALUE).

For something as simple as FizzBuzz, I would cap it there: I wouldn't create a mocked FizzBuzzer and all that. But be prepared to defend that decision (basically by saying the simplicity of the function doesn't warrant a very complex test). When I interview people, I like to suggest a not-great counter-example to an idea of theirs, and see if they can defend their idea (or possibly improve it!).

yshavit
  • 42,327
  • 7
  • 87
  • 124
  • I fully acknowledge the first part of what you said (the separation of concerns for TDD), but would the interviewer have been unimpressed at my testing outside the given 1-100 range given in the spec? I avoided that deliberately at the time. – seanhodges Mar 06 '12 at 14:09
  • @seanhodges Depends on the interviewer and how he phrased the problem. If it were me, and I explicitly said not to worry about out-of-bounds values, then you don't need to worry about them; otherwise, I would hope you'd at least ask whether we need to worry about bounds for this problem. It's great when things work given right inputs, but a vital part of software engineering is to have things fail "nicely" (quickly and understandibly) given bad inputs. – yshavit Mar 06 '12 at 14:24
  • Good point, I should have had that clarified before embarking on the test. – seanhodges Mar 06 '12 at 14:27
1

I won't claim great experience of TDD, so please don't think I'm speaking as an authority! With that in mind, here's my $0.02:

  1. I would make all those static methods instance methods, and then create a new FizzBuzz instance for each test.
  2. Get rid of generate() and just put that code into getResultAsArray(). (Very minor this.)
  3. It's fine to have constants in your unit test class. (i.e. What @APC said).

The other possible changes you mention seem like overkill to me.

One more point: FizzBuzz? Eugh! It's a very poor example question to use since it's so trivial...

Akash
  • 1,716
  • 2
  • 23
  • 43
vaughandroid
  • 4,315
  • 1
  • 27
  • 33
  • What a cryptic final sentence. – Kzqai Jun 10 '12 at 20:43
  • FizzBuzz is the perfect question for TDD and interviews, it you don't agree then you've already demonstrated that you're not a good candidate. The logic is simple to implement and understand and can be implemented either from first principals or using advanced techniques. From a TDD perspective there are a lot of boundary conditions to test and some really obvious points of confusion all in a simple requirement. Importantly for TDD it seems so simple that we tend to rush it, overlooking important aspects like the test not overly influencing the actual logic implementation. – Chris Schaller Mar 20 '21 at 00:15