Make Money with TDD - 02
It's Euro Time!
And onto the next requirement, and the reason I goofed and created a Money
object initially...
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
Next up is doing... the same thing for a different currency!
OK... Let's get the test in place.
Since we've created Dollar specific files, we'll need a new test class for our Euros.
My preference is to keep the production code in with the tests until at some time it ends out no longer in the tests, I'll just create a file EuroTests
and use it much like we did the initial test class file. Much easier to generate code and bounce between test and source.
Pondering
How do I want to approach the euro object? The requirement is identical to the Dollar. I could follow all of the tests that I did and reproduce that... or... be lazy and Copy/Paste dollar with a slight rename.
....
I'm lazy.
[TestClass]
public class EuroTests
{
[TestMethod]
public void EuroReturnsConstructedValue()
{
//Arrange
int value = new Random().Next(1, 20);
Euro subject = new(value);
//Act
int actual = subject.Value();
//Assert
actual.Should().Be(value);
}
[TestMethod]
public void EuroValueByMultiplicandShouldBeExpected()
{
//Arrange
int value = new Random().Next(1, 20);
int multiplicand = new Random().Next(1, 20);
Euro subject = new(value);
//Act
int actual = subject.Times(multiplicand);
//Assert
actual.Should().Be(value * multiplicand);
}
}
public class Euro
{
private readonly int _amount;
public Euro(int amount)
{
_amount = amount;
}
public int Value()
{
return _amount;
}
public int Times(int multiplicand)
{
return _amount * multiplicand;
}
}
And now we have our Euro.
That's ... a pain in the ass if we want to add another currency. And how will we convert... Things are so identical... we can't leave it.
But... TECHNICALLY... 2nd requirement is done.
5 USD * 2 = 10 USD10 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
Test Identified Design Smell
One thing I like about writing a lot of very small tests, you start to see design smells from how you interact with the tests. Ignoring the fact we copied the Dollar class, we copied the tests and made very minor changes.
Anytime you can apply the same tests to multiple objects... there's probably a missing abstraction. At the very least, some CLEAR behavioral duplication. Maybe it's ok. Listen to the code, it generally knows what it's talking about.
In this case, we're getting a strong whiff of "missing abstraction" from being able to apply the same test format to multiple objects.
Let's clean this up.
Now we're Money
This is where we can create the Money
object and have different currencies.
We're going to work with the Euro object and tests since they are still in the same file.
A Euro
is an amount and a currency. The currency is not represented aside from the class name. Let's make it explicit in the Euro object itself.
The constructor can be changed to look like public Euro(int amount, string currency = "EUR")
And we'll go ahead and store that
public Euro(int amount, string currency = "EUR")
{
_amount = amount;
_currency = currency;
}
I'm not a fan of having values that shouldn't change be publicly accessible like currency is here. Someone COULD override it.
Let's go ahead and use my favorite; Constructor Chaining
public Euro(int amount):this(amount, "EUR"){}
private Euro(int amount, string currency)
{
_amount = amount;
_currency = currency;
}
I've marked this commit with F!!
. It's a feature, and it's dangerous. While we have tests making sure things are working, we have no tests around the currency itself. DANGER!!!
We can do the same thing for Dollar
.
public Dollar(int amount) : this(amount, "USD") { }
private Dollar(int amount, string currency)
This is also a F!!
commit. For the same reason.
Extract that Money
Now that we've got the structure in place; we can extract a super class from Dollar
that has the methods and variables shared between Dollar
and Euro
... which... is... yeah... all of them.
public abstract class Money
{
private readonly int _amount;
private readonly string _currency;
protected Money(int amount, string currency)
{
_amount = amount;
_currency = currency;
}
public int Value()
{
return _amount;
}
public int Times(int multiplicand)
{
return _amount * multiplicand;
}
}
public class Dollar : Money
{
public Dollar(int amount) : base(amount, "USD") { }
}
We'll transition Euro
over as well
public class Euro : Money
{
public Euro(int amount) : base(amount, "EUR") { }
}
All Right!
Tests are still green! We've successfully refactored out a base class!
Knowledge Classes
A lot of people, including Kent in TDDbE say that a constructor is not enough reason to have a class. Pretty sure my mentor would agree.
I don't.
If we follow the TDDbE approach, if a consumer wants money in Euro, they MUST know the currency for Euro. A lot of places are going to have the currency value hard coded. What if it changes? OK, now we have an enum
of all currency values. Everywhere still has to know the right one to use. The knowledge of what the currency for a EURO is should exist in one and only one place. There is a single point of truth for how to create a Euro. The Euro
. No where else in the entire system will ever, nor CAN ever, know what makes it a Euro vs a Dollar. That knowledge is in a single place, and is locked away from everyone else.
I call these knowledge classes. They follow very strongly with my idea of Represent the Concepts; and the Single Point of Truth (SPOT) principle.
I don't think that the Euro
should know WHAT makes up the currency code. That's too much knowledge. It just knows WHICH currency code... which should be defined somewhere else. I don't know where/how yet.
I've only got a couple examples... and no usage. Trying to extract out and generalize at this point is going to bite us in the ass. Let's leave it a little smelly and move on.
Money Tests
While Money
is getting tested via the existing tests, it's getting hit by both. That seems excessive. We should reduce this duplication.
Because I'm lazy, I'm gonna just copy/paste an existing test class and update it to be Money
.
Oh No... Money
is abstract ... ... ... How can we do this?
I like to create test private classes that allow testing of the abstract class w/o any possible baggage of actual derived classes.
private class TestMoney:Money
{
public TestMoney(int amount, string currency) : base(amount, currency)
{
}
}
It's just a pass-through to allow an instantiation of an object to test the behavior in Money
.
With everything green, we can eliminate the tests for Euro and Dollar.
But wait... They're things... shouldn't we have some tests for them?
Absolutely.
The question is, "What do these classes do?" Cause it's not much.
public class Dollar : Money
{
public Dollar(int amount) : base(amount, "USD") { }
}
There are really two things. It derives from Money
and provides a currency.
Currently we can't check the currency, so let's make sure our object derives from Money
.
[TestMethod]
public void DerivesFromMoney()
{
Money subject = new Euro(5);
}
This might seem silly, but I think it's important to be able to be confident of the inheritance. Especially since the behavior of a Euro
requires the behavior of Money
. If that changes, we want to know.
There is a way to confirm the Currency. We can create a text output of the value and currency. It's mostly for testing and debugging, but it would expose the information and we could use it.
Stringify
I don't like to override ToString
. I find that it's always uncertain what it does, or what it can do. I'm almost always looking at it, or the docs, before using it.
I tend to create methods specific to what I want instead of relying on the system to do something for me, but differently than it normally does... and maybe than any other object does... It always feels dirty when I override ToString
.
[TestMethod]
public void AsStringExists()
{
//Arrange
Money subject = new TestMoney(12, "CUR");
//Act
string actual = subject.AsString();
//Assert
}
So, let's create an AsString
method.
and passing it is simple
public string AsString()
{
return "";
}
But that's not helping us test that the currency value is correct...
[TestMethod]
public void AsStringReturnsValueAndCurrency()
{
//Arrange
Money subject = new TestMoney(12, "CUR");
//Act
string actual = subject.AsString();
//Assert
actual.Should().Be("[amount=12][currency=CUR]");
}
but this will.
public string AsString()
{
return "[amount=12][currency=CUR]";
}
and it passes!...
And now we can delete the redundant Exists
test.
Tests with them
Now that we have a method, we can add tests for Euro
and Dollar
to verify their currency.
[TestMethod]
public void AsStringReturnsValueAndCurrency()
{
//Arrange
Money subject = new Euro(12);
//Act
string actual = subject.AsString();
//Assert
actual.Should().Be("[amount=12][currency=EUR]");
}
Nice and easy to add the test, and it fails... because it's hard coded.
The easy way to do this is ... ya know... correctly. But that's boring.
public string AsString()
{
if(_currency == "EUR") return "[amount=12][currency=EUR]";
return "[amount=12][currency=CUR]";
}
is very exciting!
We can now refactor away the Derives from money test. We're confirming that in the AsString test for Euro
And we'll do it again for Dollar
.
[TestMethod]
public void AsStringReturnsValueAndCurrency()
{
//Arrange
Money subject = new Dollar(5);
//Act
string actual = subject.AsString();
//Assert
actual.Should().Be("[amount=5][currency=USD]");
}
Which gives us our failing test!
Which is quickly remedied by a hard coded hack
if (_currency == "USD") return "[amount=5][currency=USD]";
Wonderful. Now that we're green... we can refactor this pattern into a generalization.
return $"[amount={_amount}][currency={_currency}]";
and everything passes!
We know about our Knowledge
Now that we've gotten this test in place with the AsString, everything our Knowledge Classes know, we can prove is true. It inherits money, and has a currency. BAM!
Simple Stuff
This was pretty straight forward as we focused on creating a second example of behavior to be able to draw the generalization our of. With some extra work we're able to confirm all of our system and behaviors with tests.
Next Time
With our first two requirements taken care of, next up will be
5 USD * 2 = 10 USD10 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