Make Money with TDD - 02

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 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

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 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
Show Comments