Open Credo

January 13, 2017 | Software Consultancy

How to TDD FizzBuzz with JUnit Theories

The notorious FizzBuzz interview test was originally proposed as a way of weeding out candidates for programming jobs who – to put it bluntly – couldn’t program. The task is as follows:

Write a program that prints the numbers from 1 to 100. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz”.

It turns out that this problem has just enough subtlety about it to cause headaches to anyone who knows the basics but hasn’t learned how to think in nested structures.

WRITTEN BY

Dominic Fox

Dominic Fox

How to TDD FizzBuzz with JUnit Theories

FizzBuzz: a recap

The notorious FizzBuzz interview test was originally proposed as a way of weeding out candidates for programming jobs who – to put it bluntly – couldn’t program. The task is as follows:

Write a program that prints the numbers from 1 to 100. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz”.

It turns out that this problem has just enough subtlety about it to cause headaches to anyone who knows the basics but hasn’t learned how to think in nested structures. Here’s one way to get it wrong:

public String fizzBuzz(int number) {
    if (isMultiple(number, 3)) {
        return "Fizz";
    } else if (isMultiple(number, 5)) {
        return "Buzz";
    } else if (isMultiple(number, 3) && isMultiple(number, 5)) {
        return "FizzBuzz";
    } else {
        return Integer.toString(number);
    }
}

This won’t work, because in cases where the input is divisible by both 3 and 5, it will match the first condition and return “Fizz”, without ever reaching the condition which tests for both fizz-ness and buzz-ness. We can fix that by re-ordering things:

public String fizzBuzz(int number) {
    if (isMultiple(number, 3) && isMultiple(number, 5)) {
        return "FizzBuzz";
    } else if (isMultiple(number, 3)) {
        return "Fizz";
    } else if (isMultiple(number, 5)) {
        return "Buzz";
    } else {
        return Integer.toString(number);
    }
}

It seems a bit wasteful and verbose to repeat the isMultiple tests like that, though. Some people will try nested logic:

public String fizzBuzz(int number) {
    if (isMultiple(number, 3)) {
        if (isMultiple(number, 5)) {
            return "FizzBuzz";
        } else {
            return "Fizz";
        }
    } else if (isMultiple(number, 5)) {
        return "Buzz";
    } else {
        return Integer.toString(number);
    }
}

That’s not necessarily much clearer, though, and we still see isMultiple(number, 5) in two places. With a bit of thought, however, you can get to a reasonably elegant expression of the same logic, like:

public String fizzBuzz(int number) {
    boolean hasFizzness = isMultiple(number, 3);
    boolean hasBuzzness = isMultiple(number, 5);

    return hasFizzness
        ? hasBuzzness
            ? "FizzBuzz"
            : "Fizz"
        : hasBuzzness
            ? "Buzz"
            : number.toString();
}

Surprisingly many people can’t get as far as working out how to implement isMultiple, let alone how to get the logic right, let alone how to express it elegantly. To that extent, FizzBuzz is quite an effective test of elementary coding skills.

FizzBuzz and TDD

There’s another level of difficulty, though, and that’s the one you find yourself in when the interviewer stops you midway through writing the first line of the implementation and says something like “we’re a TDD shop here, how about you show us how you’d test-drive this functionality…?”

For a long time I was of the opinion that a) FizzBuzz couldn’t be meaningfully TDD’d, and b) this illustrated a common pitfall with TDD. The problem is that it’s hard to write tests for FizzBuzz that don’t essentially reproduce the functionality of FizzBuzz inside the tests, like so:

@Test
public void multiplesOfThreeButNotFiveAreFizz() {
    for (int i = 1; i <= 100; i++) {
        if ((i % 3 == 0) && !(i % 5 == 0)) {
            assertEquals("Fizz", unit.apply(i));
        }
    }
}

@Test
public void multiplesOfFiveButNotThreeAreBuzz() {
    for (int i = 1; i <= 100; i++) {
        if (!(i % 3 == 0) && (i % 5 == 0)) {
            assertEquals("Buzz", unit.apply(i));
        }
    }
}

public void multiplesOfThreeAndFiveAreFizzBuzz() {
    for (int i = 1; i <= 100; i++) {
        if ((i % 3 == 0) && (i % 5 == 0)) {
            assertEquals("FizzBuzz", unit.apply(i));
        }
    }
}

// ...and so on

The tests contain the logic of the implementation, only in an obscure and intractable form. The other approach, which feels wildly unsatisfactory, is to write something like:

@Test
public void testSomeJudiciouslyChosenValues() {
    assertEquals("Fizz", unit.apply(3));
    assertEquals("4", unit.apply(4));
    assertEquals("Buzz", unit.apply(5));
    assertEquals("FizzBuzz", unit.apply(15));
    // just in case
    assertEquals("FizzBuzz", unit.apply(30));
}

It’s hard to see this as really rigorously testing anything.

Theories to the rescue

There is a better way, however, which is to rely less on equality in testing, and to test instead for properties of inputs and outputs. Let’s review again the requirements for FizzBuzz:

  • The output must always be either a number, “Fizz”, “Buzz”, or “FizzBuzz”. No other output is valid.
  • Whenever the input is a multiple of 3, the output must contain “Fizz”.
  • Whenever the input is a multiple of 5, the output must contain “Buzz”.
  • Whenever the output is a number, it must be the number that was input.

We can express each of these as a theory, using JUnit theories as follows:

@RunWith(Theories.class)
public class FizzBuzzTest {

    @DataPoints
    public static final int[] numbers = IntStream.range(1, 100).toArray();

    private final Function<Integer, String> unit = new FizzBuzzFunction();

    @Theory
    public void validOutputTheory(int number) {
        assertTrue("Output must be valid FizzBuzz", isValidFizzBuzz(unit.apply(number)));
    }

    @Theory
    public void fizzTheory(int number) {
        assumeTrue("Number is divisible by 3", isDivisibleBy(3, number));

        assertTrue("Output must contain 'Fizz'", unit.apply(number).contains("Fizz"));
    }

    @Theory
    public void buzzTheory(int number) {
        assumeTrue("Number is divisible by 5", isDivisibleBy(5, number));

        assertTrue("Output must contain 'Buzz'", unit.apply(number).contains("Buzz"));
    }

    @Theory
    public void numberTheory(int number) {
        String fizzbuzz = unit.apply(number);
        assumeTrue("Output is a number", isANumber(fizzbuzz));

        assertTrue(
                "Output must be string representation of input number",
                number == Integer.valueOf(fizzbuzz));
    }

    private boolean isDivisibleBy(int divisor, int number) {
        return (number % divisor) == 0;
    }

    private boolean isValidFizzBuzz(String candidate) {
        return candidate.equals("Fizz")
                || candidate.equals("Buzz")
                || candidate.equals("FizzBuzz")
                || isANumber(candidate);
    }

    private boolean isANumber(String candidate) {
        return candidate.chars().allMatch(Character::isDigit);
    }
}

The @DataPoints declared at the start of the test class are a collection of values for which all of the theories in the class must be true – in this case, the numbers 1 to 100. We express each of the constraints we enumerated above as a separate theory, using an assumeTrue expression to express the “whenever X” part of the constraint.

Importantly, the class above tests all the things that must be true of a valid FizzBuzz function’s outputs, without reproducing the exact logic that the function uses to satisfy these constraints. Instead of saying what the output must be in each case, we say what it must be like.

Hamcrest is an anagram of “matchers”

This is the principle behind the venerable Hamcrest library of “matchers”, whose purpose is to enable testers to create “flexible expressions of intent”. Given appropriate Hamcrest matchers isValidFizzBuzz, isDivisibleBy and isANumber, we could have written the above as follows:

@RunWith(Theories.class)
public class FizzBuzzTest {

    @DataPoints
    public static final int[] numbers = IntStream.range(1, 100).toArray();

    private final Function<Integer, String> unit = new FizzBuzzFunction();

    @Theory
    public void validOutputTheory(int number) {
        assertThat(unit.apply(number), isValidFizzBuzz());
    }

    @Theory
    public void fizzTheory(int number) {
        assumeThat(number, isDivisibleBy(3));

        assertThat(unit.apply(number), containsString("Fizz"));
    }

    @Theory
    public void fizzTheory(int number) {
        assumeThat(number, isDivisibleBy(5));

        assertThat(unit.apply(number), containsString("Buzz"));
    }

    @Theory
    public void numberTheory(int number) {
        String fizzbuzz = unit.apply(number);
        assumeThat(fizzbuzz, isANumber());

        assertThat(Integer.valueOf(fizzbuzz), equalTo(number));
    }
}

Hamcrest matchers are somewhat more cumbersome to implement than simple assertions, but they have the useful property that they are self-describing and re-usable. As this is FizzBuzz, however, we’re probably not all that interested in re-use.

Conclusion

The lesson here is that while rigid TDD can lead to perverse test code that simply reproduces the logic of the unit under test in inverted form, or ad hoc test code that picks out a few specific cases and trusts that the rest will fall out according to the same pattern, TDD can be made simultaneously more supple and general by defining assertions which test only for the desired properties of outputs, and theories which can be tested over ranges of values.

I would be very impressed by an interview candidate who was able to separate out the underlying constraints of the task and express them in this form – although I am also dubious that even a very strong programmer would necessarily be able to do this on a whiteboard, under the stress of an interview situation.

 

This blog is written exclusively by the OpenCredo team. We do not accept external contributions.

RETURN TO BLOG

SHARE

Twitter LinkedIn Facebook Email

SIMILAR POSTS

Blog