TDD LIKE YOU MEAN IT - 06

TDD LIKE YOU MEAN IT - 06

Third Test's a Charm

As a refresher, let's remind ourselves of the requirements of FizzBuzz

Write a method that takes a whole number and returns a text representation and has the following behavior for whole numbers above 0 (does not include 0). We don't have to add any bounds checking in this.
1. Return the string form of the number
   * 1 as input returns "1"
2. If input is a multiple of 3 return "Fizz"
   * 6 as input return "Fizz"
3. If input is a multiple of 5 return "Buzz"
   * 10 as input return "Buzz"
4. If input is a multiple of 3 and 5 return "FizzBuzz"
   * 30 as input return "FizzBuzz"

and we're working on 2. If input is a multiple of 3 return "Fizz".

We have two tests in place, and are expecting to triangulate onto a generalization with this third test.

Being back at the top of the flow chart, we have a requirement, and I bet if 1*3, and 2*3 were good tests, 3*3 is gonna fail.

This means that we'll be adding a test that //Given 9 returns Fizz. main 3af8477
I'm not going to write the intent as 3*3. That doesn't communicate well. It makes us do additional mental work to get to 9, what we're actually providing.

It's true that we'd have to in the method as well... except the method name already tells us. Cognitive load (which is a term I picked up from A Philosophy of Software Design is kept low by providing the information in two forms, where each supports the other in clearly communicating the intent.

Just like the previous test, copy paste is nice and simple main e0d7bd7.
The only change internal is to update from multiplying by 2 to 3 main ffdb831.
Finally we change the name from 6 to 9.

Watching the tests run shows us the test, as expected, fails for the reason we expect with a clear error message.

We think there's a pattern here. A good way to test that is to copy and paste and see what needs to be modified.
Copy/Paste the line if (source == 2 * 3) return "Fizz"; and update it to be if (source == 3 * 3) return "Fizz";main 6415c1d

All our tests pass!

That's definitely a pattern with pretty minimal change.

Before we change this up - I don't like 3*3. It hides the difference between the multiple and the factor.  This is a great place to apply the refactor to make different things more different.
Towards this, we'll do 12 instead of 9. if I wanted to continue practicing how to handle a flakey test, I'd use 15. We can save that for another day.

There are a couple ways we could change to the new input. Modify the existing test and code, or write a new test and add code; then delete the current test.
A new test is a safer way to do it. We'll pretend to do a new test by deleting the code and modifying the test. main 6992f72

This causes us to have a failing test. And we can see it's failing with our new number.

We now update our method to be main 934df9e

public string Transform(int source)
{
    if (source == 4 * 3) return "Fizz";
    if (source == 2 * 3) return "Fizz";
    if (source == 1 * 3) return "Fizz";
    return source.ToString();
}

And we are once again passing!

We can also update the comment to reflect our new intent. main 53de6ce

We can clearly see that the request is being patterned in the code; that it's a multiple of 3.

How do we generalize this? How'd you do it before seeing my example?

I've done an implementation using a loop and division - it wasn't great. Even worse would be a loop subtracting 3. These are all going to work though. That's important. If we don't know the language operation that can do this for us; we may have to hack in something to get functionality in place and come back to it when we have more knowledge.

Part of the practice of TDD is to accept that a less than ideal answer is better than no answer. Often it's even better than spending way too much time to get the ideal answer.  I've built components that... didn't work. I spent a lot of time on pieces of that component optimizing them to ... toss them. A sub-par implementation to get end to end functionality in place would have shown me what actually matters.

The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming. - Donald Knuth.

Summarized

Premature optimization is the root of all evil

A non-optimized solution could work just fine.

That said, I do know about the mod operator. Apparently it's called the "Remainder Operator" in C#.

The remainder operator % computes the remainder after dividing its left-hand operand by its right-hand operand.
...
Console.WriteLine(5 % 4);   // output: 1
Console.WriteLine(5 % -4);  // output: 1
Console.WriteLine(-5 % 4);  // output: -1
Console.WriteLine(-5 % -4); // output: -1

We can add a new if line at the top

if (0 == source % 3) return "Fizz";

main 02e4338

Most of the time when I, and most others, write this, we do if (source % 3 == 0) return "Fizz"; I could say things about having the constant on the left (C, C++ pattern) or something clever... but no. It's to make similar things more similar.

 public string Transform(int source)
 {
     if (0 == source % 3) return "Fizz";
     if (source == 4 * 3) return "Fizz";
     if (source == 2 * 3) return "Fizz";
     if (source == 1 * 3) return "Fizz";
     return source.ToString();
 }

It makes the3 in all the checks line up. I think it's cute.

Our tests continue to pass. So we'll comment out the replaced lines main 935b6d1. With our tests still passing we're safe to delete those lines main 984d62b.

As we're refactoring - It's a great point to delete our human language intent comment main 32ef859.

Test Reduction

Let's follow our flow chart further. We're pretty good at refactoring what we have in the code.

We have a requirement. While we are pretty sure we're done - We follow the flow chart. After a lot of thought (none) I know there's no way to write a test for a multiple of 3 that won't return "Fizz". We're unable to write a failing test.

We follow the flow chart because it does explicitly tell us to refactor the tests for the requirements. While we did touch them up during the refactor phase, we didn't look at them as a whole. And we shouldn't have. We don't want to try and combine tests we're using for triangulation.
Now that we have completed the requirement, we can refactor the tests, if we want to. Earlier discussion covered some options and my thoughts on them, There's no need to cover them again. I'll continue the pattern I used for the first test. main 7cbbabf

[TestMethod]
public void GivenInputMultipleOf3ReturnsFizz()
{
    //ARRANGE
    Dictionary<int, string> regressionValues = new()
    {
        { 1 * 3, "Fizz" },
        { 2 * 3, "Fizz" },
        { 4 * 3, "Fizz" }
    };

    (int sourceInput, string expected) =
        regressionValues.ElementAt(new Random().Next(0, regressionValues.Count));

    //ACT
    string actual = Transform(sourceInput);

    //ASSERT
    actual.Should().Be(expected);
}

I copy/pasted the first test and modified the input and expected values. Which is a little amusing to me. I really enjoy when my efforts to make similar things more similar, to express intent, to keep code consistent line up in unexpected ways.

{ 1, "1" },
{ 2, "2" },
{ 4, "4" }
...
{ 1 * 3, "Fizz" },
{ 2 * 3, "Fizz" },
{ 4 * 3, "Fizz" }

Exact same set of first numbers. This was unexpected. Just amusing to see. I can also see that the same multiples will exist for the next requirement, 5. While I won't force the pattern in for the 3*5 requirement, it'll be interesting if I get there for some other reasons.

With the test green, run it a few time to make sure it doesn't obviously flake. We can now remove the number specific tests. main b75b340

Next Time

For next time we'll select our new requirement and start writing a test for it!

Show Comments