Make Money with TDD - 01
It's Money Time!
Out of our requirements, let's start with the simple one
5 USD * 2 = 10 USD
10 EUR * 2 = 20 EUR
4002 KRW / 4 = 1000.5 KRW
5 USD + 10 EUR = 17 USD
1 USD + 1100 KRW = 2200 KRW
Multiple the money!
This is actually one I don't like how L-TDD or TDDbE do it. They both start with doing the multiplication and assume that extracting the value works. SURE... but that's not my TDD.
One of the goals here is to demonstrate MY style of TDD. The other goal is to demonstrate my style of OO, with a fairly well known example.
Using ReSharper allows me to create templates to write code for me. Use tools and you write so little code. One I like to use for my tests will produce something that looks like
[TestMethod]
public void METHOD()
{
//Arrange
//Act
//Assert
}
You'll notice the 3 comment lines. This is a style of structuring a test to make what's happening really obvious. Google it for more info.
There are a couple stylistic things I do in tests. Some people have liked... others not so much.
My tests are self contained. I don't like extensive helper methods. I don't like shared objects in my tests. I like the test to be easily refactorable. I will often extract classes and have to extract test methods to a new file representing that class behavior; it's much easier if there aren't little threads all over. Not a lot of people agree with that one. I feel pretty strongly about it.
Next is that there should be a single assert. This is a guide. Sometimes there might be more; but the goal is a single assert.
There MUST BE a single act. Anything more makes the test terrible.
The violation of a single assert is when I have copy/paste of everything through the //Act
section, with ZERO modification. Then go ahead and combine the tests and have multiple asserts. If anything in the //Arrange
or //Act
is different, they should be different tests. No branching in tests.
Make it
You're gonna laugh at me for this; but this is what I prefer to do. My first test is called MoneyExists
. It's the test to make money exist.
[TestMethod]
public void DollarExists()
{
//Arrange
new Dollar();
//Act
//Assert
}
My First Test. This fails, BTW. It won't compile. Dollar
doesn't exist.
Let's make a commit for the failing test!!!
commit c4687a5970af6ec68c952c824efda124d83db5c3 (HEAD -> main)
Author: Quinn Gil <Fyzxs@users.noreply.github.com>
Date: Sat Nov 6 23:36:50 2021 -0700
T** Failing test for Dollar to exist
That's right - FAIL TEST! FAIL! There's a few reasons to do this. We can roll back a teeny bit if we need to. If we can't get from red to green in a few minutes, then we should reset and try again. We'll have learned and should be able to get there faster. My mentor gave the time limit of 15 minutes; but I've found that if you aren't getting to green in about 5 minutes, you're better off resetting. You get 3 tries in 15 minutes this way, and will learn a lot more than if you beat your head for 15 minutes.
OH MAN... I should have used the TCR script for this... I'd probably hate myself if I did. But it sure would enforce those small steps. :) OK, not using that. Back to the actual work.
I've gotten to green!
public class Dollar
{
}
There's now a Dollar
class. Doesn't do much; but it exists.
Note: I fucked up and usedMoney
instead ofDollar
to start.. I'm retroactively changing this blog post, but it'll show in the commits.
We get that green test committed! OH YEAH!!!
Take it
What's next? Well... The goal is
5 USD * 2 = 10 USD
So, we need to be able to create 5 USD.
I bet we can make a test for that.
[TestMethod]
public void TakeAmountInConstructor()
{
//Arrange
new Dollar(5);
//Act
//Assert
}
This doesn't compile. THAT'S A FAILING TEST!!! Which gets committed.
OK, let's get to green.
public class Dollar
{
public Dollar(int amount=0)
{}
}
This constructor does it very nicely for us.
We're green. We can commit!
And now to refactor. I like using the param labels. I'm going to put it in the test.
new Dollar(amount:5);
Why did I add the default value of zero? To keep the first test passing. We don't need the default, or that first test. The second test performs the same function. It's truly duplicative.
Let's get rid of the DollarExists
test.
Now we can get rid of the default value.
And we're still green!
I don't like that it's generated an int
. I'm adding a requirement to be able to handle non-whole number quantities
5 USD * 2 = 10 USD
10 EUR * 2 = 20 EUR
4002 KRW / 4 = 1000.5 KRW
5 USD + 10 EUR = 17 USD
1 USD + 1100 KRW = 2200 KRW
2.50 USD * 6 = 15 USD
That might get handled as part of division, but I want to be explicit of the expectation.
Get it
Next we want to be able to determine the value of the Dollar
.
We'll need a test for that.
[TestMethod]
public void DollarProvidesValue()
{
//Arrange
Dollar dollar = new(amount:5);
//Act
dollar.Value();
//Assert
}
value
is an undefined method. We're getting a compilation error; which is a failing test! Before I commit, I'm going to add one piece to the //Act
line.
int actual = dollar.Value();
What this does is provide additional information to the tool when I have it generate the method for me.
Without this, in C#, we'll get public void value()
as the signature. We'll have to make additional changes to have it return an type.
By adding a return expectation, the tool will use that to generate the signature we want. Less changes for me as we go forward. Makes my life easier since the tool does it for me.
Provide your intent, the tool is pretty good at doing the right thing.
And... My Kotlin is sneaking up on me... C# uses Pascal Casing.
We now have a class with a method to give us a value back
public class Dollar
{
public Dollar(int amount)
{
}
public int Value()
{
return 0;
}
}
But it doesn't. And we don't know it doesn't. All our tests are green.
Really get it
Let's get a test with an assert that makes sure we get our expected value back.
[TestMethod]
public void FiveDollarsHasValue5()
{
//Arrange
Dollar subject = new(amount:5);
//Act
int actual = subject.Value();
//Assert
actual.Should().Be(5);
}
Everything's working except I've got a red Should().Be()
... What is this I've typed? It's FluentAssertions. It makes my asserts read so very nice.
I had to install it via NuGet...
And now - Everything compiles... but the test fails!! YAY! Why does it fail?
Expected actual to be 5, but found 0.
Always check the failure reasons. Even if you KNOW it; check it.
And... what's the quickest fix for this?
THAT'S RIGHT! return 5
.
public int Value()
{
return 5;
}
We're green now.; We can refactor!
Both the TakeAmountInConstructor
and DollarProvidesValue
have their purpose duplicated in FiveDollarsHasValue
. We can delete them.
Get not 5
We can't have 5 for everything. We need to have different amounts of dollars. We COULD, being the reasonably sharp engineers we are. edit the code to do the right thing. It's not hard. It's a few lines. But no. We're sharp, but we understand that maintaining the practices that keep us safe in the code is how we stay happy.
What we're going to do is Triangulation. I'll happily admit that doing triangulation for something that's as simple as this is going to feel silly. And while I'm typing this post between writing tests, yes, it definitely adds to the time this is taking.
If you just do the tests; it takes minimally longer. Usually it's just a single extra test. It takes nearly zero time to implement when using the tools. I counted (poorly) about 2 dozen keys (excluding the method name) pressed to generate
[TestMethod]
public void SevenDollarsHasValue7()
{
//Arrange
Dollar subject = new(7);
//Act
int actual = subject.Value();
//Assert
actual.Should().Be(7);
}
And now I have a failing test. We need to make this pass.
And I get to do it the fun way!
EVIL PROGRAMMER!
There's a game I like to play when I'm coaching a team on TDD. I'm an evil programmer; which I got from Llewellyn Falco.
Being an evil programmer is where you write the simplest, dumbest, most bone-headed direct thing you can to make the test pass; but don't clean up the code.
I find this highly valuable because it builds in the habits and practices that really save us in more complex code bases.
Let's take a look at how this goes.
First; we need to actually store the amount
passed into the constructor.
private readonly int _amount;
public Dollar(int amount)
{
_amount = amount;
}
I typed no code for to do that.
Now let's get our Value
method updated to pass our test.
public int Value()
{
if (_amount == 7) return 7;
return 5;
}
OK = OK.. I can hear your complaining from the past... damn.
It's green though. All of our tests are green!! I'M COMMITTING!!!
But Why?
Let's look at the value of doing this.
We keep return 5;
unchanged. That represents our existing behavior. That's what the system currently has, we want to preserve that until we're in a green state and can refactor.
When we're writing code to make a test pass, we want to change as little as possible. If we allow ourselves to change all the code all over, how do we know what part broke if a test fails?
Right now, if some test breaks; I know it has some condition around an amount of 7; and that I didn't understand the system well enough. I can roll back my changes. Those changes are able to be deleted because I have them isolated from the rest of the functionality. This has saved me time in numerous situations when my intended change broke tests. I could delete, comment out, tweak things, all without risking the existing behavior.
Why return 7;
instead of return _amount;
? Make Similar Things More Similar. We are returning a hard coded value so I want to continue to return a hard coded value.
I could write another test, with another if
statement. I think that's A BIT silly, but not much more. When I'm coaching teams I'll have them do a third test. Right now, we have 2 examples, and they aren't the same. We KNOW what it'll be, but the code does not show us.
Ya know what... Let's go ahead and do that 3rd test.
Really? Get it... a 3rd time
I'm copy/pasting the SevenDollarHasValue7
test since it's 99% exactly what we want. I have a trick, that I learned from Woody Zuill when you copy/paste a test. (code space reduced)
We start with this; and the IDE shows we have a naming conflict.
public void SevenDollarsHasValue7(){
Dollar subject = new(7);
int actual = subject.Value();
actual.Should().Be(7);
}
What do we do first?
Most people, my former self (and forgetful self) changed the name. It'll now be NineDollarsHasValue9
. Oh shit, I'm late for a meeting.
!!!TIME PASSES!!!
OK, boring meeting... coffee barely works... What was I doing... Run the tests, it's how we figure it out... All Green! Fuck Yeah! ... But not everything was right. Our renamed test hasn't been updated for the values yet!!! We have a false-passing test.
We don't want to have a test pass that isn't reflective of the intent of the test. Let's not rename it first. Let's update the contents first
I go from the bottom up. Nice and clean, and makes the name of the test the last thing in the test you encounter.
actual.Should().Be(9);
Dollar subject = new(9);
NineDollarsHasValue9
It's a small thing; and has saved me. Not as often as other techniques have, but it has. I try to do it because it has value for me. It's fun to coach people on it as well. I've switched teams around computers after a rename and watched some stumble because the tests pass, but the functionality isn't implemented. Yes, it's a contrived situation; but it's a rare occurrence, in reality. Still worth it to do.
And with those changes, we have a failing test!
If you're paying attention to my commits, I seem to toggle between t**
and t
for my failing tests. I'm trying to use the t**
for non-compilation failures. Not sure I've managed, but that's my thinking. It's also 1am... soo... thinking may not be my strong-point right now.
And with a simple copy/paste, we have a passing test.
public int Value()
{
if (_amount == 9) return 9;
if (_amount == 7) return 7;
return 5;
}
We are all green.
Refactor
We can now see a pattern. I don't like to refactor code until the code is showing me the pattern. An if
and a return
are not a pattern. They aren't similar.
These two if
s are super similar. Being the sharp engineers we are, we can see that if _amount
is 7 or 9, we can just return amount.if(_amount == 7 || _amount == 9) return _amount
public int Value()
{
if (_amount == 7 || _amount == 9) return _amount;
if (_amount == 9) return 9;
if (_amount == 7) return 7;
return 5;
}
Still green.
public int Value()
{
if (_amount == 7 || _amount == 9) return _amount;
//if (_amount == 9) return 9;
//if (_amount == 7) return 7;
return 5;
}
Still green.
What's that? Why commented? In cause it didn't work. I could just uncomment the lines to be back at green.
Since not having those still has us at green; we can delete them.
public int Value()
{
if (_amount == 7 || _amount == 9) return _amount;
return 5;
}
You can't see it... but ReSharper is telling me I can make a change to the ||
in the if
clause. What the hell is it suggesting... if (_amount is 7 or 9) return _amount;
OH... OH... OH MY GAWD I LOVE IT!!! That's amazing. Fucking fantastic! I've been wanting that kinda conditional for 20 some fucking years! YESSSS!!!
clears throat
And this is a great example of a tool like ReSharper. The keep the tool up to date with the latest syntax and can show you improvements you may have otherwise been unware of. For myself... apparently I've been under a damn rock to not know C# got this. I feel dumb.
I'm gonna highlight that if I did the "right" thing right away, I would have never found out this cool fucking language feature. The step wise approach I use showed me something new and exciting.
You can NEVER say this approach has no value. Fucking Value Right Here!!! YEAH!!!
Right - back to the code.
At this point; Yeah... the pattern is pretty painfully obvious; we return the amount provided. So... let's refactor to do that.
public int Value()
{
return _amount;
if (_amount is 7 or 9) return _amount;
return 5;
}
Since we're completely green, we can remove the unreachable code.
Refactor Time
We now have 3 methods that cover the exact same flow. How do we handle this? We can leave them all. It "took" all of them to get there, and it will ensure we never hit a regression if we keep them. But... more code is not less code. we want less code.
One test is good enough.
The name isn't right though. The test no longer represents checking that a Five Dollar Has Value 5; it's checking that a Dollar returns the value constructed with.DollarReturnsConstructedValue
While leaving a hard coded value in is OK; I like to have the variation the original methods represented. There's a few ways to do that.
We could use DataRow
[TestMethod]
[DataRow(5)]
[DataRow(7)]
[DataRow(9)]
public void DollarReturnsConstructedValue(int input)
{
Dollar subject = new(input);
int actual = subject.Value();
actual.Should().Be(input);
}
But... I don't like those. It is still a bit limited. I've also seen them REALLY abused. Any test you want a slightly different form, or result... you get a giant mess. Or flags... It gets ugly when it deviates from the simplistic.
The option I tend to go with for 1:1 situations is a random value
[TestMethod]
public void DollarReturnsConstructedValue()
{
int value = new Random().Next(1, 20);
Dollar subject = new(value);
int actual = subject.Value();
actual.Should().Be(value);
}
This should always work, but prevents a regressions. Another way is to use a map of input and output values. Pick one at random and run it. This works really well for more complex scenarios, like FizzBuzz.
It's also a great combination of less code, but executing multiple values. I think it looks better than using [DataRow]
.
Let's Multiply
We now have a money object we can get a value out of. Now we need to be able to multiply out money.
I bet we can write a test for that.
[TestMethod]
public void FiveDollarsTimes2ShouldBe10Dollars()
{
//Arrange
Dollar subject = new(5);
//Act
int actual = subject.Times(2);
//Assert
actual.Should().Be(10);
}
This doesn't follow how the first Times
test is written in either TDDbE
or LTDD
. Both of those introduce a lot of concepts that need to be fixed before the test can pass. I did each one of them via a test. I prefer doing it this way as it's very explicit as to what I'm doing. I know where I'm going, so it doesn't add much. In the end, all the tests generated for it go away. Except my test for the Value
method, That sticks around. It's a piece of behavior that's verified, not duplicating that verification in the test for Times
.
It's a failing test, let's commit.
Now, we could totally have a test for the missing method that doesn't assert because it's a compilation error... I start to shy away from those once things are going. I do them early on for objects, and then less so when I know what I want to assert against. One of the reasons is that I'm often inclined to write the assert first.
What do you want to assert? This isn't testing that Times
exists, it's testing that when invoked, Times
returns 10. Enough pieces are in place, I don't need the TimesExists
test.
Not having that test does add an extra step to making this test pass.
public int Times(int i)
{
return 0;
}
I have to see it fail for the right reason. I have to create the method, and have it return a non-expected value. Expected actual to be 10, but found 0.
If I returned 10 right away, I would have never seen the assert fail.
Not seeing the assert fail is VERY dangerous. I've been bitten by it, and it's fun poking teams I'm coaching about it when we practice.
Check that that test fails due to the assert, and that it fails for the right reason.
This return 0
is essentially replacing the work done by the TimesExists
test. I don't think this is any faster than having that additional test. It's an extra step as part of this test; and it is easy to skip... which would mean we never saw the test fail for the reason it should.
We're OK now... We'll see how I handle it for the next method...
Now that we've seen the assert fail, let's make it pass.
public int Times(int i)
{
return 10;
}
BOO YAH!
Tests are Green!
Refactor?
What can we refactor here? Both the mentioned books point out a concept I really like, and I've used a lot.
What's 10
here? In both the test and the Times
method? It's not really 10, but our test that 2 times 5 has the value 10. Let's refactor these to be the concept they represent.
Represent the concept.
If we look at the Times
method... that input param kinda sucks for a name. I won't get into a naming fight, it's hard to name things. Except this one. Math solved the name for us. multiplicand
Let's Multiply More
And... now we need triangulation!
The steps here are
- Copy & Paste
FiveDollarsTimes2ShouldBe10Dollars
- Change the assert to ... 7*3
- Change the act to 3
- Change the constructor to 7
- Change the name to
SevenDollarsTimes3ShouldBe21Dollars
Which gives us the failing test
[TestMethod]
public void SevenDollarsTimes3ShouldBe21Dollars()
{
Dollar subject = new(7);
int actual = subject.Times(3);
actual.Should().Be(7 * 3);
}
Yay!
Let's make it Green!
public int Times(int multiplicand)
{
if (_amount == 7 && multiplicand == 3) return 7 * 3;
return 5 * 2;
}
That's right! Back to the explicitness. I do so love it.,
Refactor
Do we see patterns... While I could argue for one more test, let's be the sharp engineers we are. We recognize that 5*2
is actually the _amount
and multiplicand
. Which we can verify; since we're green, we can refactor the code
public int Times(int multiplicand)
{
if (_amount == 7 && multiplicand == 3) return 7 * 3;
if (_amount == 5 && multiplicand == 2) return 5 * 2;
throw new Exception("whoops");
}
With everything still green; we can be confident that there's a pattern.
public int Times(int multiplicand)
{
return _amount * multiplicand;
if (_amount == 7 && multiplicand == 3) return 7 * 3;
if (_amount == 5 && multiplicand == 2) return 5 * 2;
throw new Exception("whoops");
}
Resulting in the wonderfully simple
public int Times(int multiplicand)
{
return _amount * multiplicand;
}
Refactor More
We have test duplication now. I like the the Random
in the first refactored test group. Let's do that again.
[TestMethod]
public void DollarValueByMultiplicandShouldBeExpected()
{
//Arrange
int value = new Random().Next(1, 20);
int multiplicand = new Random().Next(1, 20);
Dollar subject = new(value);
//Act
int actual = subject.Times(multiplicand);
//Assert
actual.Should().Be(value * multiplicand);
}
Our earlier separation of 10
into 5*2
helped make the patterns and modification of the test much more obvious. Breaking things out into the concepts they are composed of can really help the evolution of the code.
While we're kinda working in a contrived example... It readily shows itself when using TDD in complex systems.
Money Multiplies
That's our first requirement DONE!
5 USD * 2 = 10 USD
10 EUR * 2 = 20 EUR
4002 KRW / 4 = 1000.5 KRW
5 USD + 10 EUR = 17 USD
1 USD + 1100 KRW = 2200 KRW
2.50 USD * 6 = 15 USD
Naming is hard
I realized the test class name can be updated. DollarExampleTests
isn't really it anymore... it's actually DollarTests
. Let's make it that.
And the Dollar
class kinda popped out into it's own file. Fine. That'll get renamed as well.