The examples taken up in ** Part I "Multinational Currencies" ** of the book "Test Driven Development" are based on JAVA. To deepen my understanding, I tried the same practice in Python.
By the way, the code of test-driven development practice is stored on Github below. https://github.com/ttsubo/study_of_test_driven_development/tree/master/python
The procedure for test-driven development advocated in the book "Test-Driven Development" is as follows.
--Write one small test. --Run all tests and make sure one fails. --Make small changes. --Run the test again and make sure everything is successful. --Refactor and remove duplicates.
In this practice as well, we will follow this procedure as much as possible.
The following two requirements should be realized.
--Add two different currencies to get the amount converted based on the exchange rate between the currencies. --Multiply the amount (amount per currency unit) by the number (number of currency units) to get the amount.
First, multiply the amount (amount per currency unit) by a number (number of currency units) to get the amount. We aim for a mechanism called
.
Specifically, we will proceed with coding so that $ 5 * 2 = $ 10
holds.
Create a test that confirms that $ 5 * 2 = $ 10
holds.
tests/test_money.py
from testtools import TestCase
from example.dollar import Dollar
class MoneyTest(TestCase):
def testMultiplication(self):
five = Dollar(5)
five.times(2)
self.assertEqual(10, five.amount)
Deploy the code that just loads the Dollar
class.
example/dollar.py
class Dollar:
pass
The test will fail because the Dollar
class cannot be objectified.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testMultiplication FAILED [100%]
================================== FAILURES ===================================
___________________________ MoneyTest.testMultiplication ______________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
File "/Users/ttsubo/source/study_of_test_driven_development/tests/test_money.py", line 6, in testMultiplication
five = Dollar(5)
TypeError: Dollar() takes no arguments
Define a constructor so that you can objectify the Dollar
class.
example/dollar.py
class Dollar:
def __init__(self, amount):
self.amount = amount
The test fails because the Dollar
class does not have a times
method defined.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testMultiplication FAILED [100%]
================================== FAILURES ===================================
___________________________ MoneyTest.testMultiplication ______________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
File "/Users/ttsubo/source/study_of_test_driven_development/tests/test_money.py", line 7, in testMultiplication
five.times(2)
AttributeError: 'Dollar' object has no attribute 'times'
Multiply the amount by a number to get the amount. The
times method defines it so that it can be
.
example/dollar.py
class Dollar:
def __init__(self, amount):
self.amount = amount
def times(self, multiplier):
self.amount *= multiplier
Finally, the test is successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
In Chapter 1, you get the amount by multiplying the amount (amount per currency unit) by a number (number of currency units). As a mechanism called , I tried to implement the minimum necessary. Furthermore, here, the amount is obtained by multiplying the amount (amount per currency unit) by a numerical value (number of currency units). We will extend the mechanism called
so that it can be called many times.
After confirming that $ 5 * 2 = $ 10
holds, create a test that also holds $ 5 * 3 = $ 15
.
It is assumed that the Dollar
class is made into an object once, and then the multiplication value is changed in various ways for testing.
tests/test_money.py
from testtools import TestCase
from example.dollar import Dollar
class MoneyTest(TestCase):
def testMultiplication(self):
five = Dollar(5)
product = five.times(2)
self.assertEqual(10, product.amount)
product = five.times(3)
self.assertEqual(15, product.amount)
The test fails because we haven't yet implemented the logic to get the execution result of the times
method of the Dollar
object.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testMultiplication FAILED [100%]
================================== FAILURES ===================================
___________________________ MoneyTest.testMultiplication ______________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
File "/Users/ttsubo/source/study_of_test_driven_development/python/tests/test_money.py", line 8, in testMultiplication
self.assertEqual(10, product.amount)
AttributeError: 'NoneType' object has no attribute 'amount'
Implement the logic to get the execution result of the times
method of the Dollar
object.
--Create a new Dollar
object when calling the times
method of the Dollar
object
--Save the multiplication result in the new Dollar
object instance variable ʻamount`
example/dollar.py
class Dollar:
def __init__(self, amount):
self.amount = amount
def times(self, multiplier):
return Dollar(self.amount * multiplier)
The test is now successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
In Chapter 2, you get the amount by multiplying the amount (amount per currency unit) by a number (number of currency units). The mechanism called has been extended so that it can be called many times. Here, we'll add a mechanism to make sure that
one $ 5 is worth the same as the other $ 5. By the way, the identity of the object is confirmed by utilizing the special method
eq` of python.
Create a test to make sure that one $ 5 is equivalent to the other $ 5
.
tests/test_money.py
from testtools import TestCase
from example.dollar import Dollar
class MoneyTest(TestCase):
def testMultiplication(self):
five = Dollar(5)
product = five.times(2)
self.assertEqual(10, product.amount)
product = five.times(3)
self.assertEqual(15, product.amount)
def testEquality(self):
self.assertTrue(Dollar(5) == Dollar(5))
self.assertFalse(Dollar(5) == Dollar(6))
The test fails because there is still no mechanism in the Dollar
class to verify that` one $ 5 is equivalent to the other $ 5.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality FAILED [ 50%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
================================== FAILURES ===================================
___________________________ MoneyTest.testMultiplication ______________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
File "/Users/ttsubo/source/study_of_test_driven_development/python/tests/test_money.py", line 13, in testEquality
self.assertTrue(Dollar(5) == Dollar(5))
File "/Users/ttsubo/.pyenv/versions/3.8.0/lib/python3.8/unittest/case.py", line 765, in assertTrue
raise self.failureException(msg)
AssertionError: False is not true
Take advantage of the special method __eq__
to define` one $ 5 so that you can see that it is equivalent to the other $ 5.
example/dollar.py
class Dollar:
def __init__(self, amount):
self.amount = amount
def __eq__(self, other):
return self.amount == other.amount
def times(self, multiplier):
return Dollar(self.amount * multiplier)
The test is now successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 50%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
In Chapter 3, we've added a mechanism to make sure that one $ 5 is worth the same as the other $ 5.
Here, we will refactor the duplicated parts of the test description so far with a clear view.
In addition, change the instance variable ʻamount of the
Dollar` object to a private member.
MoneyTest: testMultiplication
.The times
method of the Dollar
class returns a Dollar
object that holds its own amount multiplied by the multiplier
argument in the Dollar object's instance variable ʻamount. In the test description I've written so far, it's hard to tell that the
Dollar` object is returned, so I'll refactor it to improve readability.
tests/test_money.py
from testtools import TestCase
from example.dollar import Dollar
class MoneyTest(TestCase):
def testMultiplication(self):
five = Dollar(5)
self.assertEqual(Dollar(10), five.times(2))
self.assertEqual(Dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Dollar(5) == Dollar(5))
self.assertFalse(Dollar(5) == Dollar(6))
The tests are still successful after the refactoring.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 50%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
Change the instance variable ʻamount of the
Dollar` object to a private member.
example/dollar.py
class Dollar:
def __init__(self, amount):
self.__amount = amount
def __eq__(self, other):
return self.__amount == other.__amount
def times(self, multiplier):
return Dollar(self.__amount * multiplier)
The test is successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 50%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
Up to Chapter 4, as one of the multi-currencies, multiply the amount (amount per currency unit) related to ** dollar currency ** by a numerical value (number of currency units) to obtain the amount. We have realized the mechanism of
. Here, as another currency, ** Franc currency ** will realize the same mechanism.
Add a test so that you can see the same thing as the dollar currency test in the franc currency.
tests/test_money.py
from testtools import TestCase
from example.dollar import Dollar
from example.franc import Franc
class MoneyTest(TestCase):
def testMultiplication(self):
five = Dollar(5)
self.assertEqual(Dollar(10), five.times(2))
self.assertEqual(Dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Dollar(5) == Dollar(5))
self.assertFalse(Dollar(5) == Dollar(6))
def testFrancMultiplication(self):
five = Franc(5)
self.assertEqual(Franc(10), five.times(2))
self.assertEqual(Franc(15), five.times(3))
Deploy the code that just loads the Franc
class.
example/franc.py
class Franc:
pass
The test fails because the Franc
class cannot be objectified.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication FAILED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
================================== FAILURES ===================================
___________________________ MoneyTest.testMultiplication ______________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
File "/Users/ttsubo/source/study_of_test_driven_development/tests/test_money.py", line 16, in testFrancMultiplication
five = Franc(5)
TypeError: Franc() takes no arguments
We will define the Franc
class by referring to what we did in Chapter 1.
example/franc.py
class Franc:
def __init__(self, amount):
self.__amount = amount
def __eq__(self, other):
return self.__amount == other.__amount
def times(self, multiplier):
return Franc(self.__amount * multiplier)
The test is successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
In Chapter 5, you get the amount by multiplying the amount (amount per currency unit) related to the franc currency by a number (number of currency units). We realized the mechanism of
, but the method corresponded by copying the whole mechanism of the dollar currency so far, so a lot of overlapping parts occurred.
As an effort to eliminate duplication, first of all, the one $ 5 that we worked on in Chapter 3 is the same value as the other $ 5
and thefive francs are the same value as the other five francs. Start the
part.
Dollar currency test Add a test so that one $ 5 is equivalent to the other $ 5
in the franc currency.
tests/test_money.py
from testtools import TestCase
from example.dollar import Dollar
from example.franc import Franc
class MoneyTest(TestCase):
def testMultiplication(self):
five = Dollar(5)
self.assertEqual(Dollar(10), five.times(2))
self.assertEqual(Dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Dollar(5) == Dollar(5))
self.assertFalse(Dollar(5) == Dollar(6))
self.assertTrue(Franc(5) == Franc(5))
self.assertFalse(Franc(5) == Franc(6))
def testFrancMultiplication(self):
five = Franc(5)
self.assertEqual(Franc(10), five.times(2))
self.assertEqual(Franc(15), five.times(3))
The added test part is also successful without any problems.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
Here you will perform the following refactorings:
--Define a new parent class Money
--Eliminate the duplication of the special method __eq__
defined in the Dollar
and Franc
classes, and make it common in the Money
class.
example/money.py
class Money:
def __init__(self, amount):
self.__amount = amount
def __eq__(self, other):
return self.__amount == other.__amount
example/dollar.py
from example.money import Money
class Dollar(Money):
def __init__(self, amount):
self.__amount = amount
def times(self, multiplier):
return Dollar(self.__amount * multiplier)
example/franc.py
from example.money import Money
class Franc(Money):
def __init__(self, amount):
self.__amount = amount
def times(self, multiplier):
return Franc(self.__amount * multiplier)
Unexpectedly, the test was wiped out. </ font>
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality FAILED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication FAILED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication FAILED [100%]
================================== FAILURES ===================================
______________________________ MoneyTest.testEquality _________________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
...(snip)
File "/Users/ttsubo/source/study_of_test_driven_development/example/money.py", line 6, in __eq__
return self.__amount == other.__amount
AttributeError: 'Dollar' object has no attribute '_Money__amount'
________________________ MoneyTest.testFrancMultiplication ____________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
...(snip)
File "/Users/ttsubo/source/study_of_test_driven_development/example/money.py", line 6, in __eq__
return self.__amount == other.__amount
AttributeError: 'Franc' object has no attribute '_Money__amount'
__________________________ MoneyTest.testMultiplication ______________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
...(snip)
File "/Users/ttsubo/source/study_of_test_driven_development/example/money.py", line 6, in __eq__
return self.__amount == other.__amount
AttributeError: 'Dollar' object has no attribute '_Money__amount'
The reason why the test failed was the timing of objectification of Dollar
, Franc
, and it was necessary to save the value in the instance variable ʻamount of the parent class
Money`, so correct the code.
example/dollar.py
from example.money import Money
class Dollar(Money):
def __init__(self, amount):
super(Dollar, self).__init__(amount)
self.__amount = amount
def times(self, multiplier):
return Dollar(self.__amount * multiplier)
example/franc.py
from example.money import Money
class Franc(Money):
def __init__(self, amount):
super(Franc, self).__init__(amount)
self.__amount = amount
def times(self, multiplier):
return Franc(self.__amount * multiplier)
This time the test was successful. It's because of test-driven development that you can immediately notice even a small refactoring mistake. </ font>
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
So far, we have supported "dollar currency" and "franc currency" as multiple currencies. The question now is, "What if you compare the ** dollar currency ** with the ** franc currency **?"
Add a test to make sure that the ** dollar currency ** and the ** franc currency ** are not equal.
tests/test_money.py
from testtools import TestCase
from example.dollar import Dollar
from example.franc import Franc
class MoneyTest(TestCase):
def testMultiplication(self):
five = Dollar(5)
self.assertEqual(Dollar(10), five.times(2))
self.assertEqual(Dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Dollar(5) == Dollar(5))
self.assertFalse(Dollar(5) == Dollar(6))
self.assertTrue(Franc(5) == Franc(5))
self.assertFalse(Franc(5) == Franc(6))
self.assertFalse(Franc(5) == Dollar(5))
def testFrancMultiplication(self):
five = Franc(5)
self.assertEqual(Franc(10), five.times(2))
self.assertEqual(Franc(15), five.times(3))
The test will fail. In other words, the result is that the dollar and the franc are equal.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality FAILED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
================================== FAILURES ===================================
______________________________ MoneyTest.testEquality _________________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
File "/Users/ttsubo/source/study_of_test_driven_development/tests/test_money.py", line 16, in testEquality
self.assertFalse(Franc(5) == Dollar(5))
File "/Users/ttsubo/.pyenv/versions/3.8.0/lib/python3.8/unittest/case.py", line 759, in assertFalse
raise self.failureException(msg)
AssertionError: True is not false
Since it was necessary to compare the Dollar
object and the Franc
object in order to perform the equivalence comparison, modify the judgment logic part of the special method __eq__
of the Money
class.
example/money.py
class Money:
def __init__(self, amount):
self.__amount = amount
def __eq__(self, other):
return (self.__amount == other.__amount
and self.__class__.__name__ == other.__class__.__name__)
This time the test was successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
In Chapters 6 and 7, we refactored the equivalence comparison. From now on, for a while, multiply the amount (amount per currency unit) by the number (number of currency units) to get the amount. Attempts to eliminate duplicates in the `mechanism.
Here, it is assumed that the following refactoring will be performed as the first step to eliminate the duplicated part of the times
method defined in the Dollar
and Franc
classes and to make it common in the Money
class. And fix the test.
--Define the class method dollar
in the Money
class to create a Dollar
object
--Define the class method franc
in the Money
class to create a Franc
object
tests/test_money.py
from testtools import TestCase
from example.money import Money
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertTrue(Money.franc(5) == Money.franc(5))
self.assertFalse(Money.franc(5) == Money.franc(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testFrancMultiplication(self):
five = Money.franc(5)
self.assertEqual(Money.franc(10), five.times(2))
self.assertEqual(Money.franc(15), five.times(3))
Here, the following refactoring is performed as the first step to eliminate the duplicated part of the times
method defined in the Dollar
and Franc
classes and to make it common in the Money
class.
--Define the class method dollar
in the Money
class to create a Dollar
object
--Define the class method franc
in the Money
class to create a Franc
object
example/money.py
from abc import ABCMeta, abstractmethod
from example.dollar import Dollar
from example.franc import Franc
class Money(metaclass=ABCMeta):
def __init__(self, amount):
self.__amount = amount
def __eq__(self, other):
return (self.__amount == other.__amount
and self.__class__.__name__ == other.__class__.__name__)
@abstractmethod
def times(self, multiplier):
pass
@classmethod
def dollar(cls, amount):
return Dollar(amount)
@classmethod
def franc(cls, amount):
return Franc(amount)
The test could not be started and an error occurred. It seems that the cause is the circular import of Python modules.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality FAILED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication FAILED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
=================================== ERRORs ====================================
ImportError while importing test module '/Users/ttsubo/source/study_of_test_driven_development/tests/test_money.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
tests/test_money.py:2: in <module>
from example.money import Money
example/money.py:2: in <module>
from example.dollar import Dollar
example/dollar.py:1: in <module>
from example.money import Money
E ImportError: cannot import name 'Money' from partially initialized module 'example.money' (most likely due to a circular import) (/Users/ttsubo/source/study_of_test_driven_development/example/money.py)
Modify the Dollar
and Franc
classes to be defined in money.py as it is due to the circular import of Python modules.
example/money.py
from abc import ABCMeta, abstractmethod
class Money(metaclass=ABCMeta):
def __init__(self, amount):
self.__amount = amount
def __eq__(self, other):
return (self.__amount == other.__amount
and self.__class__.__name__ == other.__class__.__name__)
@abstractmethod
def times(self, multiplier):
pass
@classmethod
def dollar(cls, amount):
return Dollar(amount)
@classmethod
def franc(cls, amount):
return Franc(amount)
class Dollar(Money):
def __init__(self, amount):
super(Dollar, self).__init__(amount)
self.__amount = amount
def times(self, multiplier):
return Dollar(self.__amount * multiplier)
class Franc(Money):
def __init__(self, amount):
super(Franc, self).__init__(amount)
self.__amount = amount
def times(self, multiplier):
return Franc(self.__amount * multiplier)
This time the test was successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
Continuing from the previous chapter, multiply the amount (amount per currency unit) by a number (number of currency units) to get the amount. Attempts to eliminate duplicates in the
mechanism.
Here, the Dollar
andFranc
objects are used as the first step to eliminate the duplication of the times
method defined in the Dollar
and Franc
classes and to make them common in the Money
class. Add the test testCurrency
, assuming that you apply the concept of " currency " for the purpose of distinguishing.
--Set the instance variable __currency
to" USD "when creating the Dollar
object.
--Define a currency
method so that you can reference the private member __currency
of the Dollar
object
--Set "CHF" in the instance variable __currency
when creating a Franc
object.
--Define a currency
method so that you can reference the private member __currency
of the Franc
object
tests/test_money.py
from testtools import TestCase
from example.money import Money
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertTrue(Money.franc(5) == Money.franc(5))
self.assertFalse(Money.franc(5) == Money.franc(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testFrancMultiplication(self):
five = Money.franc(5)
self.assertEqual(Money.franc(10), five.times(2))
self.assertEqual(Money.franc(15), five.times(3))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
Here, the Dollar
andFranc
objects are used as the first step to eliminate the duplication of the times
method defined in the Dollar
and Franc
classes and to make them common in the Money
class. We apply the concept of currency
, which is intended to make a distinction.
--Set the instance variable __currency
to" USD "when creating the Dollar
object.
--Define a currency
method so that you can reference the private member __currency
of the Dollar
object
--Set "CHF" in the instance variable __currency
when creating a Franc
object.
--Define a currency
method so that you can reference the private member __currency
of the Franc
object
example/money.py
from abc import ABCMeta, abstractmethod
class Money(metaclass=ABCMeta):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.__class__.__name__ == other.__class__.__name__)
@abstractmethod
def times(self, multiplier):
pass
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Dollar(amount, "USD")
@classmethod
def franc(cls, amount):
return Franc(amount, "CHF")
class Dollar(Money):
def __init__(self, amount, currency):
super().__init__(amount, currency)
self.__amount = amount
def times(self, multiplier):
return Money.dollar(self.__amount * multiplier)
class Franc(Money):
def __init__(self, amount, currency):
super().__init__(amount, currency)
self.__amount = amount
def times(self, multiplier):
return Money.franc(self.__amount * multiplier)
As expected, the test was successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 25%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 50%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 75%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
Multiply the amount (amount per currency unit) that you have started so far by a numerical value (number of currency units) to obtain the amount. Complete the attempt to eliminate the duplicated part of the mechanism called `.
Multiply the amount (amount per currency unit) by the number (number of currency units) to get the amount. Add the test
testDifferentClassEquality, assuming you've completed the refactoring to eliminate duplicates in the
mechanism.
tests/test_money.py
from testtools import TestCase
from example.money import Money, Franc
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertTrue(Money.franc(5) == Money.franc(5))
self.assertFalse(Money.franc(5) == Money.franc(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testFrancMultiplication(self):
five = Money.franc(5)
self.assertEqual(Money.franc(10), five.times(2))
self.assertEqual(Money.franc(15), five.times(3))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testDifferentClassEquality(self):
self.assertTrue(Money(10, "CHF") == Franc(10, "CHF"))
Perform the following refactoring:
--Delete the times
method defined in the Dollar
, Franc
class
--Define as a common times
method in the Money
class
--Review the method of equality comparison of Money
objects and modify the special method __eq__
--Change the Money
class, which was treated as an abstract class, assuming the creation of Money
objects.
example/money.py
class Money():
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Dollar(amount, "USD")
@classmethod
def franc(cls, amount):
return Franc(amount, "CHF")
class Dollar(Money):
def __init__(self, amount, currency):
super().__init__(amount, currency)
class Franc(Money):
def __init__(self, amount, currency):
super().__init__(amount, currency)
As expected, the test was successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 20%]
tests/test_money.py::MoneyTest::testDifferentClassEquality PASSED [ 40%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 60%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 80%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
The Dollar
and Franc
classes can also be eliminated, completing the refactoring we've done so far.
Multiply the amount (amount per currency unit) by the number (number of currency units) to get the amount. Assuming that the refactoring of the
mechanism is complete, we will also refactor the test itself.
tests/test_money.py
from testtools import TestCase
from example.money import Money
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
You don't need to define the Dollar
, Franc
classes because you can already distinguish between "** dollar currency " and " franc currency **" in the Money
object. Will be.
To complete the refactoring, remove the Dollar
, Franc
classes.
example/money.py
class Money():
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
As expected, the test was successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 33%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
From this, add two different currencies and get the converted amount based on the exchange rate between the currencies. We will realize a mechanism that satisfies the requirements of
. In addition, it seems that this requirement can be decomposed into the following two ToDo
s.
$5 + $5 = $10
-- $ 5 + 10 CHF = $ 10 (when rate is 2: 1)
First, add the two amounts to get the amount. As a mechanism of
, we are proceeding with coding so that $ 5 + $ 5 = $ 10
holds.
Add two amounts to get the amount. Add the test
testSimpleAddition to see how
works.
In addition, we will keep the following two concepts in mind for future designs.
--The concept of a Bank object
based on the idea that it should be the bank's responsibility to convert the currency
--The concept of a reduced
variable to store the conversion result obtained by applying an exchange rate
tests/test_money.py
from testtools import TestCase
from example.money import Money
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
The implementation dealt with in this chapter is quite complicated, so it will be a fairly large change, but it looks like this.
―― Add two amounts to get the amount. To implement the
mechanism, add a plus
method to the Money
class.
--A Money
object is created by inheriting the abstract class ʻExpression --The
reduce method of the
Bank` class is tentatively implemented here.
example/money.py
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def plus(self, addend):
return Money(self.__amount + addend.__amount, self.__currency)
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
example/bank.py
from example.money import Money
class Bank():
def reduce(self, source , toCurrency):
return Money.dollar(10)
example/expression.py
from abc import ABCMeta
class Expression(metaclass=ABCMeta):
pass
The test is successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 25%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 50%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 75%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [100%]
Continue to add two amounts to get the amount. As a mechanism of
, proceed with coding so that $ 5 + $ 5 = $ 10
holds.
Last time, we will start the behavior of the reduce
method of the Bank
class where it was defined as a tentative implementation.
In addition, since the efforts here are likely to be quite volume, we will proceed with the following steps.
--STEP1: Write the processing of $ 5 + $ 5
--STEP2: Materialize the temporary implementation of the reduce
method of the Bank
class
--STEP3: Materialize the temporary implementation of the reduce
method of the Bank
class (Cont.)
$ 5 + $ 5
As the first step to realize $ 5 + $ 5 = $ 10
, the processing of the part of $ 5 + $ 5
is materialized.
Add the test testPlusReturnsSum
.
tests/test_money.py
from testtools import TestCase
from example.money import Money
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
Make the following changes:
--Define a new Sum
class
--The Sum
object saves the state of two amounts:" augend "and" addend ".
--Change the plus
method of the Money
class to return the Sum
object.
example/money.py
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def plus(self, addend):
return Sum(self, addend)
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
class Sum(Expression):
def __init__(self, augend, addend):
self.augend = augend
self.addend = addend
The test is successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 20%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 40%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 60%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 80%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [100%]
reduce
method of the Bank
classWe will materialize the reduce
method of the Bank
class. Here, we will target the Sum
object.
Add the test testReduceSum
.
tests/test_money.py
from testtools import TestCase
from example.money import Money, Sum
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
def testReduceSum(self):
_sum = Sum(Money.dollar(3), Money.dollar(4))
bank = Bank()
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(7), result)
Make the following changes:
--The reduce
method of the Bank
class allows you to return the processing result of the reduce
method of the Sum
object.
--In the reduce
method of the Sum
class, add the two amounts to get the amount. Define the mechanism of
--Define the ʻamount method in the
Moneyclass so that the private member
__amount` can be referenced from the outside.
example/money.py
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def plus(self, addend):
return Sum(self, addend)
def amount(self):
return self.__amount
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
class Sum(Expression):
def __init__(self, augend, addend):
self.augend = augend
self.addend = addend
def reduce(self, toCurrency):
amount = self.augend.amount() + self.addend.amount()
return Money(amount, toCurrency)
example/bank.py
class Bank():
def reduce(self, source , toCurrency):
return source.reduce(toCurrency)
The test is successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 16%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 33%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 50%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 66%]
tests/test_money.py::MoneyTest::testReduceSum PASSED [ 83%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [100%]
reduce
method of the Bank
class (Cont.)We will materialize the reduce
method of the Bank
class. Here, we are targeting the Money
object.
Add the test testReduceMoney
.
tests/test_money.py
from testtools import TestCase
from example.money import Money, Sum
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
def testReduceSum(self):
_sum = Sum(Money.dollar(3), Money.dollar(4))
bank = Bank()
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(7), result)
def testReduceMoney(self):
bank = Bank()
result = bank.reduce(Money.dollar(1), "USD")
self.assertEqual(Money.dollar(1), result)
Make the following changes:
--The reduce
method of the Bank
class allows you to return the processing result of the reduce
method of the Money
object.
--Define the reduce
method of the Money
class (provisional implementation)
--Define the abstract method reduce
in the abstract class ʻExpression to force the
reducemethod definition in the
Money class and the
Sum` class.
example/money.py
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def plus(self, addend):
return Sum(self, addend)
def reduce(self, toCurrency):
return self
def amount(self):
return self.__amount
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
class Sum(Expression):
def __init__(self, augend, addend):
self.augend = augend
self.addend = addend
def reduce(self, toCurrency):
amount = self.augend.amount() + self.addend.amount()
return Money(amount, toCurrency)
example/expression.py
from abc import ABCMeta, abstractmethod
class Expression(metaclass=ABCMeta):
@abstractmethod
def reduce(self, toCurrency):
pass
The test is successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 14%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 28%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 42%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 57%]
tests/test_money.py::MoneyTest::testReduceMoney PASSED [ 71%]
tests/test_money.py::MoneyTest::testReduceSum PASSED [ 85%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [100%]
Here, we will realize the process of converting 2 francs to 1 dollar.
--STEP1: Processing to convert 2 francs to 1 dollar (exchange rate is provisionally implemented) --STEP2: Processing to convert 2 francs to 1 dollar (implementation of exchange rate table)
We will realize the process of converting 2 francs to 1 dollar. However, it is assumed that the exchange rate-> USD: CHF = 2: 1.
Add the test testReduceMoneyDifferentCurrency
.
tests/test_money.py
from testtools import TestCase
from example.money import Money, Sum
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
def testReduceSum(self):
_sum = Sum(Money.dollar(3), Money.dollar(4))
bank = Bank()
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(7), result)
def testReduceMoney(self):
bank = Bank()
result = bank.reduce(Money.dollar(1), "USD")
self.assertEqual(Money.dollar(1), result)
def testReduceMoneyDifferentCurrency(self):
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(Money.franc(2), "USD")
self.assertEqual(Money.dollar(1), result)
Make the following changes:
--Add_ratemethod to
Bankclass (temporary implementation) --When calling the
reduce method of the
Moneyobject from the
reduce method of the
Bankclass, pass your own
Bankobject. --When calling the
reduce method of the
Sumobject from the
reduce method of the
Bankclass, pass your own
Bankobject. --Define the
rate method of the
Bank` class so that you can get the exchange rate provisionally (exchange rate-> USD: CHF = 2: 1)
example/money.py
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def plus(self, addend):
return Sum(self, addend)
def reduce(self, bank, toCurrency):
rate = bank.rate(self.__currency, toCurrency)
return Money(self.__amount / rate, toCurrency)
def amount(self):
return self.__amount
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
class Sum(Expression):
def __init__(self, augend, addend):
self.augend = augend
self.addend = addend
def reduce(self, bank, toCurrency):
amount = self.augend.amount() + self.addend.amount()
return Money(amount, toCurrency)
example/bank.py
class Bank():
def reduce(self, source , toCurrency):
return source.reduce(self, toCurrency)
def add_rate(self, fromCurrency, toCurrency, rate):
pass
def rate(self, fromCurrency, toCurrency):
return 2 if (fromCurrency == "CHF" and toCurrency == "USD") else 1
example/expression.py
from abc import ABCMeta, abstractmethod
class Expression(metaclass=ABCMeta):
@abstractmethod
def reduce(self, bank, toCurrency):
pass
The test is successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 12%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 25%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 37%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 50%]
tests/test_money.py::MoneyTest::testReduceMoney PASSED [ 62%]
tests/test_money.py::MoneyTest::testReduceMoneyDifferentCurrency PASSED [ 75%]
tests/test_money.py::MoneyTest::testReduceSum PASSED [ 87%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [100%]
Based on the exchange rate table, we will realize the process of converting 2 francs to 1 dollar.
Add the test testIdentityRate
.
tests/test_money.py
from testtools import TestCase
from example.money import Money, Sum
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
def testReduceSum(self):
_sum = Sum(Money.dollar(3), Money.dollar(4))
bank = Bank()
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(7), result)
def testReduceMoney(self):
bank = Bank()
result = bank.reduce(Money.dollar(1), "USD")
self.assertEqual(Money.dollar(1), result)
def testReduceMoneyDifferentCurrency(self):
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(Money.franc(2), "USD")
self.assertEqual(Money.dollar(1), result)
def testIdentityRate(self):
self.assertEqual(1, Bank().rate("USD", "USD"))
Make the following changes:
--Allow the exchange rate table to be maintained in the Bank
class
--Various rates can be added to the exchange rate table by the ʻadd_rate method of the
Bankobject. --You can refer to the required rate by the
rate method of the
Bank` object.
example/bank.py
class Bank():
def __init__(self):
self._rates = {}
def reduce(self, source , toCurrency):
return source.reduce(self, toCurrency)
def add_rate(self, fromCurrency, toCurrency, rate):
target_rate = "{0}:{1}".format(fromCurrency, toCurrency)
self._rates[target_rate] = rate
def rate(self, fromCurrency, toCurrency):
target_rate = "{0}:{1}".format(fromCurrency, toCurrency)
if fromCurrency == toCurrency:
return 1
return self._rates.get(target_rate)
The test is successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 11%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 22%]
tests/test_money.py::MoneyTest::testIdentityRate PASSED [ 33%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 44%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 55%]
tests/test_money.py::MoneyTest::testReduceMoney PASSED [ 66%]
tests/test_money.py::MoneyTest::testReduceMoneyDifferentCurrency PASSED [ 77%]
tests/test_money.py::MoneyTest::testReduceSum PASSED [ 88%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [100%]
So far, add two different currencies and get the amount converted based on the exchange rate between the currencies. We have aimed to realize a mechanism that satisfies the requirements of
. Here we will embark on a task of $ 5 + 10 CHF = $ 10 (when the rate is 2: 1)
.
Add the test testMixedAddition
.
tests/test_money.py
from testtools import TestCase
from example.money import Money, Sum
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
def testReduceSum(self):
_sum = Sum(Money.dollar(3), Money.dollar(4))
bank = Bank()
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(7), result)
def testReduceMoney(self):
bank = Bank()
result = bank.reduce(Money.dollar(1), "USD")
self.assertEqual(Money.dollar(1), result)
def testReduceMoneyDifferentCurrency(self):
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(Money.franc(2), "USD")
self.assertEqual(Money.dollar(1), result)
def testIdentityRate(self):
self.assertEqual(1, Bank().rate("USD", "USD"))
def testMixedAddition(self):
fiveBucks = Money.dollar(5)
tenFrancs = Money.franc(10)
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(fiveBucks.plus(tenFrancs), "USD")
self.assertEqual(Money.dollar(10), result)
Make the following changes:
--Modify the ʻamount derivation method in the
reducemethod of the
Sumobject --Define the
plus method in the
Sumclass --Define the abstract method
plus in the abstract class ʻExpression
and force the plus
method definition in the Money
class and the Sum
class.
example/money.py
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def plus(self, addend):
return Sum(self, addend)
def reduce(self, bank, toCurrency):
rate = bank.rate(self.__currency, toCurrency)
return Money(self.__amount / rate, toCurrency)
def amount(self):
return self.__amount
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
class Sum(Expression):
def __init__(self, augend, addend):
self.augend = augend
self.addend = addend
def reduce(self, bank, toCurrency):
amount = self.augend.reduce(bank, toCurrency).amount() + \
self.addend.reduce(bank, toCurrency).amount()
return Money(amount, toCurrency)
def plus(self, addend):
pass
example/expression.py
from abc import ABCMeta, abstractmethod
class Expression(metaclass=ABCMeta):
@abstractmethod
def plus(self, addend):
pass
@abstractmethod
def reduce(self, bank, toCurrency):
pass
The test is successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 10%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 20%]
tests/test_money.py::MoneyTest::testIdentityRate PASSED [ 30%]
tests/test_money.py::MoneyTest::testMixedAddition PASSED [ 40%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 50%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 60%]
tests/test_money.py::MoneyTest::testReduceMoney PASSED [ 70%]
tests/test_money.py::MoneyTest::testReduceMoneyDifferentCurrency PASSED [ 80%]
tests/test_money.py::MoneyTest::testReduceSum PASSED [ 90%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [100%]
Finally, add the two different currencies and get the converted amount based on the exchange rate between the currencies. We will complete the realization of a mechanism that satisfies the requirements of `.
--STEP1: Complete the plus
method of the Sum
class
--STEP2: Complete the times
method of the Sum
class
plus
method of the Sum
classComplete the plus
method of the Sum
class.
Add the test testSumPlusMoney
.
tests/test_money.py
from testtools import TestCase
from example.money import Money, Sum
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
def testReduceSum(self):
_sum = Sum(Money.dollar(3), Money.dollar(4))
bank = Bank()
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(7), result)
def testReduceMoney(self):
bank = Bank()
result = bank.reduce(Money.dollar(1), "USD")
self.assertEqual(Money.dollar(1), result)
def testReduceMoneyDifferentCurrency(self):
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(Money.franc(2), "USD")
self.assertEqual(Money.dollar(1), result)
def testIdentityRate(self):
self.assertEqual(1, Bank().rate("USD", "USD"))
def testMixedAddition(self):
fiveBucks = Money.dollar(5)
tenFrancs = Money.franc(10)
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(fiveBucks.plus(tenFrancs), "USD")
self.assertEqual(Money.dollar(10), result)
def testSumPlusMoney(self):
fiveBucks = Money.dollar(5)
tenFrancs = Money.franc(10)
bank = Bank()
bank.add_rate("CHF", "USD", 2)
_sum = Sum(fiveBucks, tenFrancs).plus(fiveBucks)
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(15), result)
Make the following changes:
--Return the Sum
object when the plus
method of the Sum
object is called
--Define the abstract method plus
in the abstract class ʻExpression and force the
plusmethod definition in the
Money class and the
Sum` class.
example/money.py
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.amount = amount
self._currency = currency
def __eq__(self, other):
return (self.amount == other.amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.amount * multiplier, self._currency)
def plus(self, addend):
return Sum(self, addend)
def reduce(self, bank, toCurrency):
rate = bank.rate(self.currency(), toCurrency)
return Money(self.amount / rate, toCurrency)
def currency(self):
return self._currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
class Sum(Expression):
def __init__(self, augend, addend):
self.augend = augend
self.addend = addend
def reduce(self, bank, toCurrency):
amount = self.augend.reduce(bank, toCurrency).amount + \
self.addend.reduce(bank, toCurrency).amount
return Money(amount, toCurrency)
def plus(self, addend):
return Sum(self, addend)
example/bank.py
class Bank():
def __init__(self):
self._rates = {}
def reduce(self, source , toCurrency):
return source.reduce(self, toCurrency)
def add_rate(self, fromCurrency, toCurrency, rate):
self._rates[(fromCurrency, toCurrency)] = rate
def rate(self, fromCurrency, toCurrency):
if fromCurrency == toCurrency:
return 1
return self._rates.get((fromCurrency, toCurrency))
The test is successful.
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 9%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 18%]
tests/test_money.py::MoneyTest::testIdentityRate PASSED [ 27%]
tests/test_money.py::MoneyTest::testMixedAddition PASSED [ 36%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 45%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 54%]
tests/test_money.py::MoneyTest::testReduceMoney PASSED [ 63%]
tests/test_money.py::MoneyTest::testReduceMoneyDifferentCurrency PASSED [ 72%]
tests/test_money.py::MoneyTest::testReduceSum PASSED [ 81%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [ 90%]
tests/test_money.py::MoneyTest::testSumPlusMoney PASSED [100%]
times
method of the Sum
classComplete the times
method of the Sum
class.
Add the test testSumTimes
.
tests/test_money.py
from testtools import TestCase
from example.money import Money, Sum
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
def testReduceSum(self):
_sum = Sum(Money.dollar(3), Money.dollar(4))
bank = Bank()
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(7), result)
def testReduceMoney(self):
bank = Bank()
result = bank.reduce(Money.dollar(1), "USD")
self.assertEqual(Money.dollar(1), result)
def testReduceMoneyDifferentCurrency(self):
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(Money.franc(2), "USD")
self.assertEqual(Money.dollar(1), result)
def testIdentityRate(self):
self.assertEqual(1, Bank().rate("USD", "USD"))
def testMixedAddition(self):
fiveBucks = Money.dollar(5)
tenFrancs = Money.franc(10)
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(fiveBucks.plus(tenFrancs), "USD")
self.assertEqual(Money.dollar(10), result)
def testSumPlusMoney(self):
fiveBucks = Money.dollar(5)
tenFrancs = Money.franc(10)
bank = Bank()
bank.add_rate("CHF", "USD", 2)
_sum = Sum(fiveBucks, tenFrancs).plus(fiveBucks)
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(15), result)
def testSumTimes(self):
fiveBucks = Money.dollar(5)
tenFrancs = Money.franc(10)
bank = Bank()
bank.add_rate("CHF", "USD", 2)
_sum = Sum(fiveBucks, tenFrancs).times(2)
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(20), result)
Make the following changes:
--Return the Sum
object when the times
method of the Sum
object is called
--Define the abstract method times
in the abstract class ʻExpression and force the
timesmethod definition in the
Money class and the
Sum` class.
example/money.py
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def plus(self, addend):
return Sum(self, addend)
def reduce(self, bank, toCurrency):
rate = bank.rate(self.__currency, toCurrency)
return Money(self.__amount / rate, toCurrency)
def amount(self):
return self.__amount
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
class Sum(Expression):
def __init__(self, augend, addend):
self.augend = augend
self.addend = addend
def reduce(self, bank, toCurrency):
amount = self.augend.reduce(bank, toCurrency).amount() + \
self.addend.reduce(bank, toCurrency).amount()
return Money(amount, toCurrency)
def plus(self, addend):
return Sum(self, addend)
def times(self, multiplier):
return Sum(self.augend.times(multiplier), self.addend.times(multiplier))
example/expression.py
from abc import ABCMeta, abstractmethod
class Expression(metaclass=ABCMeta):
@abstractmethod
def plus(self, addend):
pass
@abstractmethod
def reduce(self, bank, toCurrency):
pass
@abstractmethod
def times(self, multiplier):
pass
The tests have been successful and the coverage is generally good.
$ pytest -v --cov=example
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 8%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 16%]
tests/test_money.py::MoneyTest::testIdentityRate PASSED [ 25%]
tests/test_money.py::MoneyTest::testMixedAddition PASSED [ 33%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 41%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 50%]
tests/test_money.py::MoneyTest::testReduceMoney PASSED [ 58%]
tests/test_money.py::MoneyTest::testReduceMoneyDifferentCurrency PASSED [ 66%]
tests/test_money.py::MoneyTest::testReduceSum PASSED [ 75%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [ 83%]
tests/test_money.py::MoneyTest::testSumPlusMoney PASSED [ 91%]
tests/test_money.py::MoneyTest::testSumTimes PASSED [100%]
...(snip)
---------- coverage: platform darwin, python 3.8.0-final-0 -----------
Name Stmts Miss Cover
-------------------------------------------
example/__init__.py 0 0 100%
example/bank.py 13 0 100%
example/expression.py 11 3 73%
example/money.py 35 0 100%
-------------------------------------------
TOTAL 59 3 95%
...(snip)
With the above, I tried to experience the development of the Python version of "multinational currency" utilizing ** test-driven development **.
You can also experience test-driven development in Python in Part II "xUnit" of the book "Test-Driven Development". Actually, after experiencing it, I noticed that ** Test Driven Development ** was quite different from the test I had imagined and was completely misunderstood. This is a quote from Chapter 32, "Learn TDD" in the book! !!
Ironically, TDD is not a testing technique (Cunningham's proposal). TDD is an analytical technique, a design technique, and in fact a technique that structures all the activities of development.
Also, Appendix C, "Translator's Commentary: The Present of Test Driven Development," helped me understand TDD / BDD and was a great learning experience.
-I rewrote "Test Driven Development" in Python -Write the source of "Test Driven Development" in Python (Part I finished) -Read "Test Driven Development"
Recommended Posts