Experience Part I "Multinational Currencies" in the book "Test Driven Development" in Python

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

■ How to proceed with test-driven development

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.

■ Experience test-driven development through multinational currencies

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.

Chapter 1: Multi-Currency Money

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.

(1) First, write one small test.

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)

(2) Prepare the code to be tested

Deploy the code that just loads the Dollar class.

example/dollar.py


class Dollar:
    pass

(3) Run the test and confirm that one fails.

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

(4) Make small changes.

Define a constructor so that you can objectify the Dollar class.

example/dollar.py


class Dollar:
    def __init__(self, amount):  
        self.amount = amount

(5) Run the test again and confirm that one fails.

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'

(6) Make small changes again.

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

(7) Run the test again and make sure everything is successful. -> OK </ font>

Finally, the test is successful.

$ pytest -v

...(snip)
tests/test_money.py::MoneyTest::testMultiplication PASSED                [100%]

Chapter 2: Degenerate Objects

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.

(1) First, write one small test.

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)

(2) Run the test and confirm that one fails.

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'

(3) Make small changes.

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)

(4) Run the test again and make sure everything is successful. -> OK </ font>

The test is now successful.

$ pytest -v

...(snip)
tests/test_money.py::MoneyTest::testMultiplication PASSED                [100%]

Chapter 3: Triangulation (Equality for All)

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.

(1) First, write one small test.

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

(2) Run the test and confirm that one fails.

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

(3) Make small changes.

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)

(4) Run the test again and make sure everything is successful. -> OK </ font>

The test is now successful.

$ pytest -v

...(snip)

tests/test_money.py::MoneyTest::testEquality PASSED              [ 50%]
tests/test_money.py::MoneyTest::testMultiplication PASSED                [100%]

Chapter 4: Intentional Test (Privacy)

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.

(1) Test: Refactor 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))

(2) Run the test and make sure everything is successful.

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

(3) Make small changes.

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)

(4) Run the test and make sure everything is successful. -> OK </ font>

The test is successful.

$ pytest -v

...(snip)

tests/test_money.py::MoneyTest::testEquality PASSED              [ 50%]
tests/test_money.py::MoneyTest::testMultiplication PASSED                [100%]

Chapter 5: When you dare to break the principle (Franc-ly Speaking)

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.

(1) First, write one small test.

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

(2) Prepare the code to be tested

Deploy the code that just loads the Franc class.

example/franc.py


class Franc:
    pass

(3) Run the test and confirm that one fails.

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

(4) Make small changes.

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)

(4) Run the test and make sure everything is successful. -> OK </ font>

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

Chapter 6: If you notice a lack of testing (Equality for All, Redux)

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

(1) First, write one small test.

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

(2) Run the test and make sure everything is successful. -> OK </ font>

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

(3) Refactor and remove duplicates.

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)

(4) Run the test again and make sure everything is successful. -> NG </ font>

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'

(5) Refactor to address the cause of the test failure.

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)

(6) Run the test again and make sure everything is successful. -> OK </ font>

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

Chapter 7: Translate Doubts into Tests (Apples and Oranges)

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 **?"

(1) First, write one small test.

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

(2) Execute the test. -> NG </ font>

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

(3) Address the cause of the test failure.

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

(4) Run the test again and make sure everything is successful. -> OK </ font>

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

Chapter 8: Hide Implementation (Makin ’Objects)

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.

(1) First, write a small test.

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

(2) Perform refactoring.

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)

(3) Execute the test. -> NG </ font>

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)

(4) Address the cause of the test failure.

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)

(5) Run the test again and make sure everything is successful. -> OK </ font>

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

Chapter 9: Adjusting Stride Length (Times We ’re Livin’ In)

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.

(1) First, write a small test.

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

(2) Perform refactoring.

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)

(3) Run the test and make sure everything is successful. -> OK </ font>

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

Chapter 10: Ask the Test (Interesting Times)

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

(1) First, write a small test.

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

(2) Perform refactoring.

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)

(3) Run the test and make sure everything is successful. -> OK </ font>

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

Chapter 11: The Root of All Evil

The Dollar and Franc classes can also be eliminated, completing the refactoring we've done so far.

(1) First, write a small test.

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

(2) Perform refactoring.

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

(3) Run the test and make sure everything is successful. -> OK </ font>

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

Chapter 12: Design and Metaphor (Addition, Finally)

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

  • $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.

(1) First, write a small test.

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)

(2) Make small changes.

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

(3) Run the test and make sure everything is successful. -> OK </ font>

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

Chapter 13: Implementation Guided Tests (Make It)

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

STEP1: Write the processing of $ 5 + $ 5

As the first step to realize $ 5 + $ 5 = $ 10, the processing of the part of $ 5 + $ 5 is materialized.

(1) First, write a small test.

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)

(2) Make small changes.

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

(3) Run the test and make sure everything is successful. -> OK </ font>

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

STEP2: Materialize the temporary implementation of the reduce method of the Bank class

We will materialize the reduce method of the Bank class. Here, we will target the Sum object.

(1) First, write a small test.

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)

(2) Make small changes.

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)

(3) Run the test and make sure everything is successful. -> OK </ font>

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

STEP3: Materialize the temporary implementation of the reduce method of the Bank class (Cont.)

We will materialize the reduce method of the Bank class. Here, we are targeting the Money object.

(1) First, write a small test.

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)

(2) Make small changes.

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

(3) Run the test and make sure everything is successful. -> OK </ font>

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

Chapter 14: Learning and Regression Tests (Change)

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)

STEP1: Processing to convert 2 francs to 1 dollar (exchange rate is provisionally implemented)

We will realize the process of converting 2 francs to 1 dollar. However, it is assumed that the exchange rate-> USD: CHF = 2: 1.

(1) First, write a small test.

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)

(2) Make small changes.

Make the following changes:

--Add_ratemethod toBankclass (temporary implementation) --When calling thereduce method of the Moneyobject from thereduce method of the Bankclass, pass your ownBankobject. --When calling thereduce method of the Sumobject from thereduce method of the Bankclass, pass your ownBankobject. --Define therate 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

(3) Run the test and make sure everything is successful. -> OK </ font>

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

STEP2: Processing to convert 2 francs to 1 dollar (implementation of exchange rate table)

Based on the exchange rate table, we will realize the process of converting 2 francs to 1 dollar.

(1) First, write a small test.

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

(2) Make small changes.

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

(3) Run the test and make sure everything is successful. -> OK </ font>

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

Chapter 15: Test and Compiler (Mixed Currencies)

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

(1) First, write a small test.

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)

(2) Make small changes.

Make the following changes:

--Modify the ʻamount derivation method in the reducemethod of theSumobject --Define theplus method in the Sumclass --Define the abstract methodplus 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

(3) Run the test and make sure everything is successful. -> OK </ font>

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

Chapter 16: Testing for Future Readers (Abstraction, Finally)

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

STEP1: Complete the plus method of the Sum class

Complete the plus method of the Sum class.

(1) First, write a small test.

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)

(2) Make small changes.

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

(3) Run the test and make sure everything is successful. -> OK </ font>

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

STEP2: Complete the times method of the Sum class

Complete the times method of the Sum class.

(1) First, write a small test.

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)

(2) Make small changes.

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

(3) Run the test and make sure everything is successful. -> OK </ font>

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

■ At the end ...

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.

■ Reference URL

-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