TDD LIKE YOU MEAN IT - 07

TDD LIKE YOU MEAN IT - 07

Once again, from the top

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 technically still working on 2. If input is a multiple of 3 return "Fizz".

An interesting aside as we go through this process - I haven't had to update the flow chart for the past couple of tests - It seems to be getting stabilized for our TDD flow. No guarantees it'll stay as it is; but... it's definitely a flow we can follow for some testing.

Since we haven't seen it for a while, I definitely want to include it here for reference.

And now - The flow.

We're, technically, currently working on a requirement, which we can't write a failing test for, leading us to move onto the next requirement of 3. If input is a multiple of 5 return "Buzz". The simplest test to consider is, "Given 5, will it return Buzz?" - and ... no. It will not.
Great! We have our test.
To make sure we keep our eye on our intent - We want to implement something for "Given input of 5 get 'Buzz' returned" - which we'll add as a comment. main 9983b84

Now to create our test scaffolding. How do we want to do that?

Did you jump right to "Fresh Test"? Ask yourself, 'Why?", if you did. I'm definitely not saying it's wrong; it serves as an opportunity to check our habits. I definitely wanted to do "Fresh Test" right away. When I ask myself why... it's the unfortunate answer of "because it's how we've been doing it - first requirement test is a fresh teste". If that valid though? We have to consider this. It'll be different across code. Even across FizzBuzz exercises it's different. I've done this exercise where only the very first test was a fresh test.

If our instincts are going for "Fresh Test", let's consider what "Copy Paste" would look like. We'll use the "Multiple of 3" test as our example copy paste test. All the code I'll be writing/editing right now is only in Typora for this post. Not in the IDE or actually running, so there's a little bit of an assumption of correctness as we work through this thought experiment.

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

The flow chart says to update the contents of the method to match our intent. OK... The only change is the dictionary; which would now look like.

{ 1 * 5, "Buzz" }

Sure, we can get that passing.

What do we do at the refactor phase? Since it's a single map, the map is removed during "Minimize Elements" refactor. And we're back to how we construct a fresh test. Maybe we could copy/paste and drop the dictionary at that point... sounds like extra fiddling around.

What about creating a single test and expanding the dictionary? We'd have to intentionally not minimize elements, but sure. We can skip some refactoring if we have a high confidence we'd just reverse it shortly.

The next test would ad {2 * 5} to the dictionary. Sure - And that will fai--- Will it? What if it passes? It might. A literal 50/50 chance. (I'm ignoring the psuedo aspect of it being psuedo random).

We might get a false positive there. Not very useful. If it did fail, and we make a change, and it passes... was that our change or ... Anyway - I expect you understand that having a non-constant test trying to triangulate behavior is not the best way to get things going.

I won't find any fault in a copy paste of the method and updating it to be something like

[TestMethod]
public void GivenInputMultipleOf3ReturnsFizz()
{
    //ARRANGE
	int sourceInput = 5;
    string expected = "Buzz";

    //ACT
    string actual = Transform(sourceInput);

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

Because ... well... that's exactly what  we should end up with. In this scenario, what do you want to practice?

  • Editing the test-  which we get practice on during the triangulation of the requirement
  • Assert First - Which we've only been practicing on the first test.

Of course, we can absolutely practice on "Assert First" on all of the tests, but then we don't get the practice of editing. Focusing on one or the other is GREAT. When we'd use this as a kata, we'd always have a primary and secondary focus. We would sometimes focus on  writing the Assert First, so we'd always do a fresh test. Other times we'd focus on "no mouse" to improve our skill with our IDE. Note: I'm still using things I learned during those sessions years later. Become better with your IDE, you'll be a more effective developer.

As this is a kinda a guide of how to apply all areas, I'll continue using the first test as a "Fresh Test" practice.

Fresh Test

Let's get that fresh test written. I have my template to generate the template, and I'll fill in the name Given5ReturnsBuzz main cc3757b and then be in position to write my assert main f832a04.

//Given input of 5 get 'Buzz' returned
[TestMethod]
public void Given5ReturnsBuzz()
{
    //ARRANGE

    //ACT

    //ASSERT
    actual.Should().Be("Buzz");
}

main 85883db

Which we can then ... follow the flowchart to "Fill out rest of the test".

[TestMethod]
public void Given5ReturnsBuzz()
{
    //ARRANGE
    int sourceInput = 5;
    string expected = "Buzz";

    //ACT
    string actual = Transform(sourceInput);

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

main 175f76e

Which fails with an informative message showing us that it's the right reason.

I do this exercise a lot - I can feel myself wanting to just jump into the code I know we'll end up with. This is something I have to reign in all the time. It's not a bad thing, but it leads to us getting ahead of ourselves and making mistakes.

Pairing and Mobbing is a great way to get support to not jump ahead like that.

To get the test to pass, we add a condition for the input and return what the assert expects.

public string Transform(int source)
{
    if (source == 5) return "Buzz";
    if (0 == source % 3) return "Fizz";
    return source.ToString();
}

main ec280cf

Which gets our test passing.

Refactor time

Is there anything to refactor? While our first tests don't often have much, there's a couple things I see.

//Given input of 5 get 'Buzz' returned

[TestMethod]
public void Given5ReturnsBuzz()
{
    //ARRANGE
    int sourceInput = 5;
    string expected = "Buzz";

    //ACT
    string actual = Transform(sourceInput);

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

public string Transform(int source)
{
    if (source == 5) return "Buzz";
    if (0 == source % 3) return "Fizz";
    return source.ToString();
}

What do you see?

We can clean up our human language comment - definitely. main 1cca2d4 Make sure you clean up any extra white space. We have the refactor steps for that, but always easier to do it when you're removing the line that might leave it. This is what we started with

}


//Given input of 5 get 'Buzz' returned

[TestMethod]

Which means I have a little extra space between the top method and the comment. Whoops.

This is part of why I try to clean up whitespace as I'm doing other things in that area of the code. I don't trust I'll catch them all in the refactor phase. Then my code starts to look unkempt.

If we just deleted the comment, we end up with

}




[TestMethod]

Which is pretty egregious. Even if we delete the whole line

}



[TestMethod]

Still a bit. When we do our best to keep the whitespace as desired as we do other tasks, we get it nicely cleaned up to be

}

[TestMethod]

Without having to try and remember it clean it up in a later refactor phase.  main dd2e482

More?

[TestMethod]
public void Given5ReturnsBuzz()
{
    //ARRANGE
    int sourceInput = 5;
    string expected = "Buzz";

    //ACT
    string actual = Transform(sourceInput);

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

public string Transform(int source)
{
    if (source == 5) return "Buzz";
    if (0 == source % 3) return "Fizz";
    return source.ToString();
}

What else?

I'm being a little sneaky here; let's compare test names - since they aren't incldued in my snippets.

GivenInputReturnsStringOfInput
GivenInputMultipleOf3ReturnsFizz
Given5ReturnsBuzz

Our current seems different. I'm sure you can see why; but I'll break it down a little.

Given Input             Returns StringOfInput
Given Input MultipleOf3 Returns Fizz
Given       5           ReturnsBuzz

Breaking this down actually shows us there's a difference in them all we might be able make more similar. My thought was that our latest test needed to change to GivenInput5ReturnsBuzz; Which I still think is correct for it. But the first test is now missing that "middle" section. Input doesn't mean the same thing in all 3 tests.

It's actually more like our latest tests current name.

Given       Input       Returns StringOfInput
Given Input MultipleOf3 Returns Fizz
Given       5           ReturnsBuzz

Which means we can go two ways - Do we include Input everywhere, or just in the first and remove it from the MultipleOf3 test.
I lean towards removing it. Given basically means Given the input ofso we've got a little duplication in our middle test name.

This is a huge reason I stop and break things down to really look at them. I'm discovering this as I'm writing it. Totally did not expect this change.

In the attempts to clarify why I was going to add Input to our latest test, I'm actually going to remove it from our middle test. We're removing duplication, improving communication, and making similar things more similar. A single word removed (or added) can have a huge impact on the code; it's definitely worth the time to consider it.

GivenInputReturnsStringOfInput
GivenMultipleOf3ReturnsFizz
Given5ReturnsBuzz

There's our new name(s). main f37b701

And?

There's a little pull from the code to make the two if statements more similar.

public string Transform(int source)
{
    if (source == 5) return "Buzz";
    if (0 == source % 3) return "Fizz";
    return source.ToString();
}

Getting alignment is nice, but I think trying to do so now won't be justifiable. Yes, we KNOW where we're going, and that's rare. We want to practice waiting until the code shows us how to make it more similar.

Test Complete

And that's our first test for our 3rd requirement. Again - No modifications to the flow chart. I think it's working pretty well for us.

We are not done, so we'll plan for a next post for our next test!

Next Time

I bet you can figure out what'll happen in the next round. It'll probably be very close to this round.

Show Comments