Chapter 2. Multi-currency Money

Followed fast and followed faster

Edgar Allen Poe, The Raven

Did the RED-GREEN-REFACTOR cycle we followed in Chapter 1 seem a tad too slow?

A response of “heck yes!” (or some rhyming phrase) is understandable!

The goal of test-driven development isn’t to force us to go slow. Or fast, for that matter. Its goal is to allow us to go at a pace we’re comfortable with: speeding up when we can, slowing down when we should.

In this chapter, we’ll introduce additional currencies and the ability to multiply and divide money in any currency. Let’s see if we can kick up the pace a bit.

Enter the Euro

The second item on our list of features introduces a new currency:

5 USD x 2 = 10 USD

10 EUR x 2 = 20 EUR

4002 KRW / 4 = 1000.5 KRW

5 USD + 10 EUR = 17 USD

1 USD + 1100 KRW = 2200 KRW

This indicates that we need a more general entity than the Dollar we created in the previous chapter. Something like Money, which encapsulates an amount (which we already have) and a currency (which we do not yet have). Let’s write tests to flush out this new feature.

Go

Let’s write a new test in money_test.go. This test requires that when a struct representing “10 Euros” is multiplied by 2, we get “20 Euros”:

func TestMultiplicationInEuros(t *testing.T) {
    tenEuros := Money{amount: 10, currency: "EUR"}
    twentyEuros := tenEuros.Times(2)
    if twentyEuros.amount != 20 {
        t.Errorf("Expected 20, got: [%d]", twentyEuros.amount)
    }
    if twentyEuros.currency != "EUR" {
        t.Errorf("Expected EUR, got: [%s]", twentyEuros.currency)
    }
}

The test expresses the concepts of “10 Euros” and “20 Euros” with struct instances containing a currency as well as amount.

By now, we know that when we run this test, we’ll get an error notifying us of undefined: Money. We can eliminate this by introducing a new struct:

type Money struct {
    amount   int
    currency string
}

We now get the error type Money has no field or method Times, which we know how to get around. We define a Times method for Money:

func (m Money) Times(multiplier int) Money {
    return Money{amount: m.amount * multiplier, currency: m.currency}
}

Yay! Green tests again.

JavaScript

Let’s write a test for representing a Money object with amount and currency. We verify that when an object representing “10 Euros” is multiplied by 2, we get “20 Euros”. We define this test at the very end of test_money.js:

tenEuros = new Money(10, "EUR");
twentyEuros = tenEuros.times(2);
assert.strictEqual(twentyEuros.amount, 20);
assert.strictEqual(twentyEuros.currency, "EUR");

By now, we anticipate the ReferenceError: Money is not defined error we get when running the tests. We can eliminate this by introducing a new class named Money with the minimally desired behavior, that is: a constructor that initializes both amount and currency, and a times method that multiplies the amount with a given multiplier and returns a new Money object.

class Money {
    constructor(amount, currency) {
        this.amount = amount;
        this.currency = currency;
    }

    times(multiplier) {
        return new Money(this.amount * multiplier, this.currency);
    }
}

Yay! Both our tests are now green.

Python

Let’s add a new test in the TestMoney class. This test would verify that multiplying an object representing “10 Euros” by 2 gives us an object representing “20 Euros”:

  def testMultiplicationInEuros(self):
    tenEuros = Money(10, "EUR")
    twentyEuros = tenEuros.times(2)
    self.assertEqual(20, twentyEuros.amount)
    self.assertEqual("EUR", twentyEuros.currency)

By now, we anticipate the NameError: name 'Money' is not defined error we get when we run the tests. We can get rid of this error by introducing a new Money class with the minimally necessary behavior. This means an __init__ method that initializes both amount and currency, and a times method that returns a new Money object whose amount is a product of the multiplier and the amount of the original Money object:

class Money:
  def __init__(self, amount, currency):
    self.amount = amount
    self.currency = currency

  def times(self, multiplier):
    return Money(self.amount * multiplier, self.currency)

Yay! Both our tests are now green.

Keeping code DRY

Wait a minute: didn’t we just create a horrendous duplication in our code? The new entity we created to represent Money subsumes what we wrote earlier for Dollar. This can’t possibly be good. A oft-quoted rule in writing code is the DRY principle: Don’t Repeat Yourself.

Recall the RED-GREEN-REFACTOR cycle. What we did in the previous section got us to green, but we haven’t done the necessary refactoring yet. Let’s remove the duplication in the code while keeping our tests green.

Go

We realize that the Money struct can do everything that the Dollar struct can, and more. Money has currency, which Dollar does not.

Let’s delete the Dollar struct and its Times method.

When we do this, the TestMultiplication test predictably breaks with an undefined: Dollar error. Let’s refactor that test to use Money instead:

func TestMultiplicationInDollars(t *testing.T) {
    fiver := Money{amount: 5, currency: "USD"}
    tenner := fiver.Times(2)
    if tenner.amount != 10 {
        t.Errorf("Expected 10, got: [%d]", tenner.amount)
    }
    if tenner.currency != "USD" {
        t.Errorf("Expected USD, got: [%s]", tenner.currency)
    }
}

Both tests now pass. Notice that we renamed the test to be more descriptive: TestMultiplicationInDollars.

JavaScript

The Money class can do everything that Dollar does, and more. This means that we can delete the Dollar class in its entirety.

When we do this and run the tests, we get our old friendly error: ReferenceError: Dollar is not defined. Let’s refactor the first test to use Money instead:

fiver = new Money(5, "USD");
tenner = fiver.times(2);
assert.strictEqual(tenner.amount, 10);
assert.strictEqual(tenner.currency, "USD");

Both tests now pass.

Python

The Money class’s functionality is a superset of that of the Dollar class. Which means we don’t need the latter. Let’s delete the Dollar class in its entirety.

Having done this, we get the familiar NameError: name 'Dollar' is not defined message when we run the tests. Let’s refactor the first test to use Money instead of the erstwhile Dollar:

  def testMultiplicationInDollars(self):
    fiver = Money(5, "USD")
    tenner = fiver.times(2)
    self.assertEqual(10, tenner.amount)
    self.assertEqual("USD", tenner.currency)

Both tests now pass. Notice that we renamed the test to be more descriptive: testMultiplicationInDollars.

Didn’t we just say “Don’t Repeat Yourself”?!

Hmm. The two tests — the one for Dollars and the one for Euros — are very similar. The currencies and amounts vary, but they test pretty much the same feature.

Repetition in code comes in varied forms. Sometimes we have identical lines of code (perhaps caused by “copy pasta” programming). In these cases, extracting the common lines to a function or method is what we need to do. At other times, we have parts of code that are not identical, but conceptually similar. This is the case with our two tests.

We could delete one of the tests and still feel confident about our code. However, we also want to safeguard ourselves against accidental regression in our code. Recall that our very first implementation used hard-coded numbers (10 or 5 * 2). Having two distinct tests with different values ensures that we won’t accidentally go back to that naive implementation.

Tip

Regression — “a return to a primitive or less developed state" — is a common theme in writing software. Having a battery of tests is a reliable way to ensure that we don’t break existing features as we build new ones.

Let’s keep both test cases for now. We’ll add an item to the end of our checklist noting our desire to remove redundancy in tests. We’ll revisit this item later, after we address division.

Here’s our feature list:

5 USD x 2 = 10 USD

10 EUR x 2 = 20 EUR

4002 KRW / 4 = 1000.5 KRW

5 USD + 10 EUR = 17 USD

1 USD + 1100 KRW = 2200 KRW

Remove redundant Money multiplication tests

Divide and Conquer

The next requirement is to allow division. On the surface, it looks very similar to multiplication. We know from elementary mathematics that dividing by x is the same as multiplying by 1⁠/⁠x. 1

Let’s test-drive this new feature and see how our code evolves. By now, we are getting into the groove of starting with a failing test. As an indicator of our growing confidence, we’ll introduce two new things in our test:

  1. A new currency: Korean Won (KRW), and

  2. Numbers with fractional parts, e.g. 1000.5

Go

Let’s write our new test for division.

func TestDivision(t *testing.T) {
    originalMoney := Money{amount: 4002, currency: "KRW"}
    actualMoneyAfterDivision := originalMoney.Divide(4)
    expectedMoneyAfterDivision := Money{amount: 1000.5, currency: "KRW"}
    if expectedMoneyAfterDivision != actualMoneyAfterDivision {
        t.Errorf("Expected  [%+v] Got: [%+v]",
            expectedMoneyAfterDivision, actualMoneyAfterDivision)
    }
}

Notice that we wrote this test a bit differently. We define variables for the two structs: actualMoneyAfterDivision and expectedMoneyAfterDivision. And instead of comparing amount and currency separately, we compare the two structs as a whole. If the structs don’t match, we print them both.

Tip

In Go, printing a struct with the %+v format “verb” prints the struct’s field names as well as values.

We anticipate the type Money has no field or method Divide error we get when we run this test. Let’s define this missing method, taking our cue from the existing Times method:

func (m Money) Divide(divisor int) Money {
    return Money{amount: m.amount / divisor, currency: m.currency}
}

Ah! The test fails with a new error: constant 1000.5 truncated to integer.

It’s clear that we need to change the amount field in the Money struct so that it can hold fractional values. The float64 data type is appropriate for this purpose:

type Money struct {
    amount   float64
    currency string
}

This gives us new errors when we run our test:

... invalid operation: m.amount * multiplier (mismatched types float64 and int)
... invalid operation: m.amount / divisor (mismatched types float64 and int)
Tip

Using an IDE can be useful because it flags syntax errors and type errors without any need to run the tests.

We need to modify our arithmetic operations (multiplication and division) to use the same data type for all operands. We know from our domain that the multipliers and divisors are likely to be integers (number of shares of a stock) whereas the amount can be a fractional number (trading price of a particular stock). Let’s use this knowledge to convert the multiplier and divisor to float64 before using them in our arithmetic operations. We can do this by calling the float64() function:

func (m Money) Times(multiplier int) Money {
    return Money{amount: m.amount * float64(multiplier), currency: m.currency}
}

func (m Money) Divide(divisor int) Money {
    return Money{amount: m.amount / float64(divisor), currency: m.currency}
}

Now we get different wrong type failures

... Errorf format %d has arg tenner.amount of wrong type float64
... Errorf format %d has arg twentyEuros.amount of wrong type float64

A careful reading of the error messages reveals that we are using the wrong format “verb” in our earlier tests to print the amount field. Since our newest test — TestDivision — successfully compares entire struct`s, we can refactor our earlier two multiplication tests to do something similar. This way, we'll side-step the whole issue of having used the incorrect formatting "verb" for `float64 type.

Here’s how TestMultiplicationInDollars looks after changing its assertion statement. (The other test, TestMultiplicationInEuros, needs similar changes.)

func TestMultiplicationInDollars(t *testing.T) {
    fiver := Money{amount: 5, currency: "USD"}
    actualResult := fiver.Times(2)
    expectedResult := Money{amount: 10, currency: "USD"}
    if actualResult != actualResult {
        t.Errorf("Expected [%+v], got: [%+v]", expectedResult, actualResult)
    }
}
Tip

If compilation or assertion failures crop up during a test run, pay attention to the error messages.

After these changes, all our tests are green.

JavaScript

Let’s write our new test for division at the end of test_money.js.

originalMoney = new Money(4002, "KRW")
actualMoneyAfterDivision = originalMoney.divide(4)
expectedMoneyAfterDivision = new Money(1000.5, "KRW")
assert.deepStrictEqual(actualMoneyAfterDivision, expectedMoneyAfterDivision)

Notice that we wrote this test a bit differently. We define variables for the two objects: actualMoneyAfterDivision and expectedMoneyAfterDivision. And instead of comparing amount and currency separately, we compare the two objects at once using the deepStrictEqual method in assert.

Tip

In Node.js’s assert module, the deepStrictEqual method compares two objects and their child objects for equality using the JavaScript === operator. 2

We anticipate the TypeError: originalMoney.divide is not a function error we get when we run this test. So let’s define this missing method, taking inspiration from the existing times method. 3

class Money {

...

    divide(divisor) {
        return new Money(this.amount / divisor, this.currency);
    }
}

Yay! The tests are all green. JavaScript’s dynamic types make implementing this feature easier than languages with static typing.

Python

Let’s write our new test for division in class TestMoney:

  def testDivision(self):
    originalMoney = Money(4002, "KRW")
    actualMoneyAfterDivision = originalMoney.divide(4)
    expectedMoneyAfterDivision = Money(1000.5, "KRW")
    self.assertEqual(expectedMoneyAfterDivision.amount,
                      actualMoneyAfterDivision.amount)
    self.assertEqual(expectedMoneyAfterDivision.currency,
                      actualMoneyAfterDivision.currency)

Notice that we wrote this test a bit differently. We define variables for the two objects: actualMoneyAfterDivision and expectedMoneyAfterDivision.

We anticipate the AttributeError: 'Money' object has no attribute 'divide' message we get when we run this test. So let’s define this missing method, taking our cue from the existing times method: 4

  def divide(self, divisor):
    return Money(self.amount / divisor, self.currency)

Yay! The tests are green. Python is a dynamically (and strongly) typed language. This makes implementing this feature easier than languages with static typing.

Cleaning Up

Let’s finish off this chapter with a bit of house cleaning, while keeping the tests green.

Go

We now have three tests with three assertion blocks, each of which is a 3-line if block. Except for the variable names in each test, the if blocks are identical. This duplication is ripe for removal via extracted into a helper function, which we can call assertEqual.

Tip

“Extract method” or “Extract function” is a common refactoring. It involves replacing common blocks of code with a call to a new function/method that encapsulates the block of code once.

func assertEqual(t *testing.T, expected Money, actual Money) {
    if expected != actual {
        t.Errorf("Expected  [%+v] Got: [%+v]", expected, actual)
    }
}

The function’s body is identical to any of the three existing if blocks. We can now call this function from each of the three tests. The TestDivision function is shown below:

func TestDivision(t *testing.T) {
    originalMoney := Money{amount: 4002, currency: "KRW"}
    actualResult := originalMoney.Divide(4)
    expectedResult := Money{amount: 1000.5, currency: "KRW"}
    assertEqual(t, expectedResult, actualResult)
}

We can modify the TestMultiplicationInEuros and TestMultiplicationInDollars tests similarly.

JavaScript

The assertion using deepStrictEqual that we used for our last test is elegant: it compares the two objects (expected value and actual value) at once. We can use it for our two other tests.

While we’re doing it, we can also address a subtle assumption in these two lines of code in our tests:

tenner = fiver.times(2);
...
twentyEuros = tenEuros.times(2);

From the perspective of the test, it is a bit presumptuous to assume that multiplying 5 Dollars or 10 Euros by two will yield 10 Dollars or 20 Euros, respectively. Indeed, that’s the very thing the tests purport to verify. We can improve our tests by inlining the call to the times method, thereby saving ourselves the trouble of naming the variable:

fiveDollars = new Money(5, "USD");
tenDollars = new Money(10, "USD");
assert.deepStrictEqual(fiveDollars.times(2), tenDollars);
Tip

“Inline variable” is a refactoring that replaces a named variable with an directly evaluated (usually anonymous) variable.

We can refactor the test for multiplying Euros similarly.

Python

Comparing two Money objects piecemeal is verbose and tedious. In our tests, we verify that the amount and currency fields of Money objects are equal, over and over. Wouldn’t it be nice to be able to compare two Money objects directly in a single line of code?

In Python, object equality is ultimately resolved by an invocation of the __eq__ method. By default, this method returns true if the two object references being compared in fact point to the same object. This is a very strict definition of equality: it means that an object is only equal to itself, not any other object, even of the two objects have the same state.

Important

The default implementation of __eq__ method means that in Python, two object references are equal if and only if they point to the same object. That is: https://docs.python.org/3/reference/datamodel.html#object.eq[equality is determined by reference, not by value].

Fortunately, it is not only possible but recommended to override the __eq__ method when needed. Let us explicitly override this method within the definition of our Money class:

class Money:

...

  def __eq__(self, other):
    return self.amount == other.amount and self.currency == other.currency

After defining the __eq__ method, we can compare Money objects in a single line.

While we’re refactoring, we can also address a subtle assumption implicit in how we named a couple of variables in our tests:

tenner = fiver.times(2)
...
twentyEuros = tenEuros.times(2)

From the test’s perspective, it isn’t a given that multiplying 5 Dollars or 10 Euros by two will yield 10 Dollars or 20 Euros, respectively. Indeed, that’s the very thing the test exists to validate. We can improve our tests by doing an inline-variable refactoring, along with the single-line assertion that we can now write.

Here’s the complete testMultiplicationInDollars :

  def testMultiplicationInDollars(self):
    fiveDollars = Money(5, "USD")
    tenDollars = Money(10, "USD")
    self.assertEqual(tenDollars, fiveDollars.times(2))

We initialize fiveDollars and tenDollars explicitly. We then verify that multiplying the former by 2 yields an object that’s equal to the latter. We also do it in one line, keeping our code readable and succinct.

The other two tests can be refactored similarly.

Committing Our Changes

We have written a couple more tests and the associated code to make them green. Time to commit these changes to our local Git repository.

git add . 1
git commit -m "feat: division and multiplication features done" 2
1

Add all files, including all changes in them, to the Git index

2

Commit the Git index to the repository with the given message

At this point, we have two commits in our Git history, a fact we can verify by examining the output of git log:

commit 1e43b6e6731407a810601d973c83b406249f4d59 (HEAD -> main) 1
Author: Saleem Siddiqui ...
Date:   Sun Mar 7 12:58:47 2021 -0600

    feat: division and multiplication features done 2

commit bb31b94e90029ddeeee89f3ca0fe099ea7556603 3
Author: Saleem Siddiqui ...
Date:   Sun Mar 7 12:26:06 2021 -0600

    chore: first green test
1

New SHA hash for our second commit, which represents the HEAD of the Git repository

2

The message we used for our second commit

3

[.small]#The SHA hash for our previous commit from Chapter 1 #

Where We Are

In this chapter, we built a second feature, division, and modified our design to deal with numbers with fractions. We have introduced a Money entity that allows us to consolidate how Dollars and Euros (and potentially other currencies) are multiplied by a number. We have a couple of passing tests. We have also cleaned up our code along the way.

Important

Depending on the specific data types and language, floating point arithmetic can cause problems of overflow/underflow. If needed, the problems can be surfaced via tests, and then solved — using RGR cycle. We also refactored our code to make it succinct and expressive.

With a couple more features crossed off our list, we’re ready to look at adding up amounts in different currencies — which will get our attention in the next chapter.

Here’s where we are in our feature list:

5 USD x 2 = 10 USD

10 EUR x 2 = 20 EUR

4002 KRW / 4 = 1000.5 KRW

5 USD + 10 EUR = 17 USD

1 USD + 1100 KRW = 2200 KRW

Remove redundant Money multiplication tests

Go

Here’s how the file money_test.go looks right now:

package main

import (
    "testing"
)

func TestMultiplicationInDollars(t *testing.T) {
    fiver := Money{amount: 5, currency: "USD"}
    actualResult := fiver.Times(2)
    expectedResult := Money{amount: 10, currency: "USD"}
    assertEqual(t, expectedResult, actualResult)
}

func TestMultiplicationInEuros(t *testing.T) {
    tenEuros := Money{amount: 10, currency: "EUR"}
    actualResult := tenEuros.Times(2)
    expectedResult := Money{amount: 20, currency: "EUR"}
    assertEqual(t, expectedResult, actualResult)
}

func TestDivision(t *testing.T) {
    originalMoney := Money{amount: 4002, currency: "KRW"}
    actualResult := originalMoney.Divide(4)
    expectedResult := Money{amount: 1000.5, currency: "KRW"}
    assertEqual(t, expectedResult, actualResult)
}

func assertEqual(t *testing.T, expected Money, actual Money) {
    if expected != actual {
        t.Errorf("Expected  [%+v] Got: [%+v]", expected, actual)
    }
}

type Money struct {
    amount   float64
    currency string
}

func (m Money) Times(multiplier int) Money {
    return Money{amount: m.amount * float64(multiplier), currency: m.currency}
}

func (m Money) Divide(divisor int) Money {
    return Money{amount: m.amount / float64(divisor), currency: m.currency}
}

JavaScript

Here’s how the test_money.js file looks at this point:

const assert = require('assert');

class Money {
    constructor(amount, currency) {
        this.amount = amount;
        this.currency = currency;
    }

    times(multiplier) {
        return new Money(this.amount * multiplier, this.currency);
    }

    divide(divisor) {
        return new Money(this.amount / divisor, this.currency);
    }
}

fiveDollars = new Money(5, "USD");
tenDollars = new Money(10, "USD");
assert.deepStrictEqual(fiveDollars.times(2), tenDollars);

tenEuros = new Money(10, "EUR");
twentyEuros = new Money(20, "EUR");
assert.deepStrictEqual(tenEuros.times(2), twentyEuros);

originalMoney = new Money(4002, "KRW")
expectedMoneyAfterDivision = new Money(1000.5, "KRW")
assert.deepStrictEqual(originalMoney.divide(4), expectedMoneyAfterDivision)

Python

Here’s how the test_money.py file looks right now:

import unittest

class Money:
  def __init__(self, amount, currency):
    self.amount = amount
    self.currency = currency

  def times(self, multiplier):
    return Money(self.amount * multiplier, self.currency)

  def divide(self, divisor):
    return Money(self.amount / divisor, self.currency)

  def __eq__(self, other):
    return self.amount == other.amount and self.currency == other.currency

class TestMoney(unittest.TestCase):
  def testMultiplicationInDollars(self):
    fiveDollars = Money(5, "USD")
    tenDollars = Money(10, "USD")
    self.assertEqual(tenDollars, fiveDollars.times(2))

  def testMultiplicationInEuros(self):
    tenEuros = Money(10, "EUR")
    twentyEuros = Money(20, "EUR")
    self.assertEqual(twentyEuros, tenEuros.times(2))

  def testDivision(self):
    originalMoney = Money(4002, "KRW")
    expectedMoneyAfterDivision = Money(1000.5, "KRW")
    self.assertEqual(expectedMoneyAfterDivision, originalMoney.divide(4))

if __name__ == '__main__':
    unittest.main()

1 ∀ x ≠ 0, i.e. as long as x isn’t zero … thank you, all the math teachers, for what you do!

2 The === operator tests whether both the values and the types of the two objects being compared are equal. See https://www.w3schools.com/nodejs/met_assert_deepstrictequal.asp

3 The ECMAScript standard defines a method as a “function that is the value of a property [of an object]” https://www.ecma-international.org/ecma-262/11.0/index.html#sec-method

4 The Python standard defines a method as “bound function objects”. That is, methods are always associated with objects, whereas functions are not. https://docs.python.org/3/c-api/method.html

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.137.164