And Repeat
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 1. Return the string form of the number
We're not done with our work, so back to the top of the flow chart we go
We're going to create a simple test! And while it is the next step; it's missing something. What test? Sure we can get the intent, but how do we keep thinking up tests?
I like this part to be explicit. It's a huge part of the routine when practicing; I'm sure it's been drilled into my students heads
What's a failing test can you write for the current requirement?
We don't really have requirements defined in the flow yet. Let's get that in.
OK, Back to the top. Are we working on a requirement? - Yes.
And we're back to What's a failing test can you write for the current requirement?
Which is a question I want to have in this flow chart. It's how I determine that there really is another test.
If you're familiar with PlantUml (plantUml.com) you might realize that I'm not a fan of empty nodes. Personal preference. If you're not familiar with PlantUML... I recommend you do so.
Getting to that second test
Let's walk our flow chart. We're back to Time for a new test and we are currently working on a new requirement. We now get to the question - "Can we write a failing test for the current requirement?"
Well... Let's think about it. Our requirements is 1. Return the string form of the number. We can mentally iterate the options.
Will a test with an input of 1 fail?
No.
Will a test with an input of 2 fail?
Yes.
And we can write a failing test for our current requirement. Onto Create test method scaffolding
How?
You might not think that the way you create your test method is important, but it can be very informative. The simpliest bit of information is that any tests you can copy/paste with some small modifications show there's commonality. This might suggest there's a missing abstraction, or other refactoring that can be done. It might also suggest that it's simply a place to keep an eye on. Copy and Paste tests are harbingers of things to come, even if we don't know when or how.
We could also re-write the entire test and never use copy and paste. Which is fine. We have the refactor phase to look for duplication, or very similar things, and can decide what to do with it then.
Because it's part of what I want to add to the flow chart; I'm gonna copy/paste our test.
Now we have 2 identical tests that look like this (only showing one because we don't need the duplication).
[TestMethod]
public void GivenInt1ShouldReturnString1()
{
//ARRANGE
int valueToTransform = 1;
string transformedValue = "1";
//ACT
string actual = Transform(valueToTransform);
//ASSERT
actual.Should().Be(transformedValue);
}
How do we update this? Let's start the same way - "Add test intent in human language".
We can think back to the first intent we wrote and make an updated version if it Given an integer of 2 should return string of 2
.
I like these at the top of the method, but wherever works best is the right palce.
You'll proibably notice that our method names are STILL identical. That's right. It's very intentional. I get this from Woody Zuill. Change the contents of a copy pasted test before you change the name.
"Why?" you ask; happy to tell you.
These are simplistic tests. But if the first thing we did was change the name - the code will compile, the test will run, and it will pass. We've just put the tests into a false state. The (renamed) test GivenInt2ShouldReturnString2
is passing. But it's not doing what the method name says it is.
"Pffft, I'll never miss that". Sure... maybe. I've missed it in simple tests like this. When you're working in a real codebase and get distracted, pulled into a meeting, go to lunch... and comeback. Maybe the next day... Maybe it's not you. When the code lies to us, we're going to make mistakes in the code due to our expecations being wrong.
To help with that, we don't let the test run until we have the assertion we want able to run. It's part of why we do Assert First. If we get the assert in, then we KNOW the assert HAS to work for the test to pass. If we don't put it in and things look green... maybe the test is expected to fail via a thrown exception... I may not know, and ... there may be nothing to clear that up, if I even look. Asserts are protection for the code. Don't let code run without proper assert(s) in place.
In this test, we don't have to modify the assert line. The input and expected are in the //ARRANGE
section. We'll modify them to match the intent of the method that we've defined in human language.
[TestMethod]
public void GivenInt1ShouldReturnString1()
{
//Given an integer of 2 should return string of 2
//ARRANGE
int valueToTransform = 2;
string transformedValue = "2";
//ACT
string actual = Transform(valueToTransform);
//ASSERT
actual.Should().Be(transformedValue);
}
[main: a2a810d]
Now, we can update the name. How? Should we rename it to the intent? Should we make it TestMethodFoo
? If we're following this practice structure, we have our intent - It's best to rename it to match the intent.
Without flow chart updated to our include our approach to copy/paste
We will run all the tests!
And see our new test fail, while our old test continues to work. This is why we run ALL the tests.
Did it fail for the asserted reason? Yes it did. We need to check this, every time. I recently worked on an example that... I failed to check it every time. I went through 2 or 3 tests and they weren't running. The test runner never picked them up and ... I wasn't foillowing TDD as I failed to confirm RED. I never checked the test results.
They weren't running!!! It's easy to make the mistake when you're in the groove and focused on code. Pairing and Mobbing is such a huge benefit to the little things like this.
Anyway; we are failing for the asserted reason. The failure message is informative enough.
Make the simplest code change
Here our step is to just return what the assert expects.
public string Transform(int _)
{
return "2";
}
Which, is obviously not the "right" answer, but let's follow our guide explicity. It's how we make it better.
And our new test passes
And we can see why it's crucial to see ALL tests run... and pass. Clearly the step of "... see the new one pass" should be "... see all tests pass".
We did what the simplest code change asked... clearly it's not quite enough. There's always going to be slightly different rules for the very first test against a method. We can just return the expected value. Beyond that, we need to add a check for the test input and then return the test expectation.
public string Transform(int _)
{
if (_ == 2) return "2";
return "1";
}
... I'll be completely honest here, I did not expect using _
to work in the if
condition. That's ... surprising. But... sure... Our Test passes!
This is an approach that gets a lot of questions.
Why use the if? Why not just to what'll work for all cases?
Which is a good question. And the answer is that "We'll be wrong". The right thing here is, probably, obvious. Especially if you're familiar with C#. More complex methods are definitely not going to have the obvious answer.
If we get it wrong and a test fails; what does the flow chart tell us to do?
Remove Changes
How easy is it to remove changes if you've modified the existing functionality? You've gotta re-work the code to get back to a known good state. If we use this if
block to encapsulate the changes we're making for this test then we're able to undo those changes FAST. Guaranteed.
Using this structure allows us to have confidence we can put the system back into a known good state without any question. We can also see exactly what we're doing for the test we're working on.
There's always the caveat that this is a simple and contrived example. This practice of creating blocks of code per test help us in triangulation. As our tests grow, the code tells us what the general form is. We don't have to guess and edit - The code will let us know when the general form starts to show itself. When that pattern starts to show itself, we then refactor that pattern into some general form, or abstraction.
Back to our IDE - All of our tests pass
And clearly _
is... like... the least informative variable name I've ever seen. I'm kind impressed. Looking at the flow chart, we're at our refactor phase - so let's get that renamed. Let's get ANY intent in there.
As mentioned before; I've had A LOT of conversations around the name of this... I'm gonna go with source
today. Is this the best? No. Is it even good? ... I don't have anything I like better. It's good enough for a variable used only once.
As we go through our refactoring phase... I've found I'm not a huge fan of the layout. I've been fiddling with it and have something that I think helps communicate the intent of the refactor section a little better.
We'll walk through these and after updating the parameter name to improve intent... It looks pretty good.
Duplication, or minimizing elements, shows us that the human language intent in our second test is no longer needed.
[main ad39dad](https://github.com/Fyzxs/Blog_Tdd_like_you_mean_it/commit/ad39dad)
We definitely has some duplication in our tests.I don't think we have enough info to make any reduction to them yet. Also, we don't want to de-duplicate our tests until the requirement is complete. That'd ruin our triangulation approach.
We're now at the point we can ask
Are there any patterns in the code?
And ... no? Right now we do't really have any patterns. Patterns can fall into "duplication" but aren't ... quite. I like to keep it separate. Things like method names and human langauge intent are duplications. Patterns in the code, aren't always. They're related, but I see duplication as things we can reduce and patterns are things we can extract from.
Since we don't see a pattern we... don't know what to do.
Summary
The second test was pretty quick. We kept to mostly the same steps as before. A little extra in how to make the test pass. A process of editing a copy and paste test. Great things to practice.
The next part of this practice will get the third test in place which will show us how to safely edit our code as we triangulate a generic solution.