├── chapter-01 ├── moneys │ ├── __init__.py │ └── dollar.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-02 ├── moneys │ ├── __init__.py │ └── dollar.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-03 ├── moneys │ ├── __init__.py │ └── dollar.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-04 ├── moneys │ ├── __init__.py │ └── dollar.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-05 ├── moneys │ ├── __init__.py │ ├── franc.py │ └── dollar.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-06 ├── moneys │ ├── __init__.py │ ├── franc.py │ ├── dollar.py │ └── money.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-07 ├── moneys │ ├── __init__.py │ ├── franc.py │ ├── dollar.py │ └── money.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-08 ├── moneys │ ├── __init__.py │ ├── franc.py │ ├── dollar.py │ └── money.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-09 ├── moneys │ ├── __init__.py │ ├── franc.py │ ├── dollar.py │ └── money.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-10 ├── moneys │ ├── __init__.py │ ├── franc.py │ ├── dollar.py │ └── money.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-11 ├── moneys │ ├── __init__.py │ └── money.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-12 ├── moneys │ ├── __init__.py │ ├── expression.py │ ├── bank.py │ └── money.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-13 ├── moneys │ ├── __init__.py │ ├── bank.py │ ├── total.py │ └── money.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-14 ├── moneys │ ├── __init__.py │ ├── exchanger.py │ ├── total.py │ ├── bank.py │ └── money.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-15 ├── moneys │ ├── __init__.py │ ├── exchanger.py │ ├── bank.py │ ├── total.py │ └── money.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── chapter-16 ├── moneys │ ├── __init__.py │ ├── exchanger.py │ ├── bank.py │ ├── total.py │ └── money.py ├── tests │ ├── __init__.py │ └── moneytests.py └── README.md ├── README.md └── .gitignore /chapter-01/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-01/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-02/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-02/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-03/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-03/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-04/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-04/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-05/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-05/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-06/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-06/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-07/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-07/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-08/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-08/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-09/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-09/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-10/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-10/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-11/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-11/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-12/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-12/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-13/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-13/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-14/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-14/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-15/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-15/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-16/moneys/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter-16/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chapter-01/README.md: -------------------------------------------------------------------------------- 1 | ## 第1章 仮実装 2 | 3 | -------------------------------------------------------------------------------- /chapter-12/moneys/expression.py: -------------------------------------------------------------------------------- 1 | """式を表す""" 2 | from abc import ABCMeta 3 | 4 | class Expression(metaclass=ABCMeta): 5 | """式(演算)を表します。""" 6 | pass 7 | -------------------------------------------------------------------------------- /chapter-08/moneys/franc.py: -------------------------------------------------------------------------------- 1 | """fran通貨""" 2 | from .money import Money 3 | 4 | class Franc(Money): 5 | """フラン通貨を表します。""" 6 | def times(self, multiplier: int) -> Money: 7 | """通貨変換""" 8 | return Franc(self._amount * multiplier) 9 | -------------------------------------------------------------------------------- /chapter-08/moneys/dollar.py: -------------------------------------------------------------------------------- 1 | """dollar通貨""" 2 | from .money import Money 3 | 4 | class Dollar(Money): 5 | """ドル通貨を表します。""" 6 | def times(self, multiplier: int) -> Money: 7 | """通貨変換""" 8 | return Dollar(self._amount * multiplier) 9 | -------------------------------------------------------------------------------- /chapter-10/moneys/franc.py: -------------------------------------------------------------------------------- 1 | """fran通貨""" 2 | from .money import Money 3 | 4 | class Franc(Money): 5 | """フラン通貨を表します。""" 6 | 7 | def __init__(self, amount: int) -> None: 8 | """初期化""" 9 | Money.__init__(self, amount, "CHF") 10 | -------------------------------------------------------------------------------- /chapter-10/moneys/dollar.py: -------------------------------------------------------------------------------- 1 | """dollar通貨""" 2 | from .money import Money 3 | 4 | class Dollar(Money): 5 | """ドル通貨を表します。""" 6 | 7 | def __init__(self, amount: int) -> None: 8 | """初期化""" 9 | Money.__init__(self, amount, "USD") 10 | -------------------------------------------------------------------------------- /chapter-13/moneys/bank.py: -------------------------------------------------------------------------------- 1 | """銀行""" 2 | from .money import Expression, Money 3 | 4 | class Bank(object): 5 | """銀行を表します。""" 6 | def reduce(self, source: Expression, currency: str) -> Money: 7 | """式を単純な形に変形する""" 8 | return source.reduce(currency) 9 | -------------------------------------------------------------------------------- /chapter-12/moneys/bank.py: -------------------------------------------------------------------------------- 1 | """銀行""" 2 | from .expression import Expression 3 | from .money import Money 4 | 5 | class Bank(object): 6 | """銀行を表します。""" 7 | def reduce(self, source: Expression, to: str) -> Money: 8 | """式を単純な形に変形する""" 9 | return Money.dollar(10) 10 | -------------------------------------------------------------------------------- /chapter-06/moneys/franc.py: -------------------------------------------------------------------------------- 1 | """fran通貨""" 2 | from __future__ import annotations 3 | from .money import Money 4 | 5 | class Franc(Money): 6 | """フラン通貨を表します。""" 7 | def times(self, multiplier: int) -> Franc: 8 | """通貨変換""" 9 | return Franc(self._amount * multiplier) 10 | -------------------------------------------------------------------------------- /chapter-07/moneys/franc.py: -------------------------------------------------------------------------------- 1 | """fran通貨""" 2 | from __future__ import annotations 3 | from .money import Money 4 | 5 | class Franc(Money): 6 | """フラン通貨を表します。""" 7 | def times(self, multiplier: int) -> Franc: 8 | """通貨変換""" 9 | return Franc(self._amount * multiplier) 10 | -------------------------------------------------------------------------------- /chapter-06/moneys/dollar.py: -------------------------------------------------------------------------------- 1 | """dollar通貨""" 2 | from __future__ import annotations 3 | from .money import Money 4 | 5 | class Dollar(Money): 6 | """ドル通貨を表します。""" 7 | def times(self, multiplier: int) -> Dollar: 8 | """通貨変換""" 9 | return Dollar(self._amount * multiplier) 10 | -------------------------------------------------------------------------------- /chapter-07/moneys/dollar.py: -------------------------------------------------------------------------------- 1 | """dollar通貨""" 2 | from __future__ import annotations 3 | from .money import Money 4 | 5 | class Dollar(Money): 6 | """ドル通貨を表します。""" 7 | def times(self, multiplier: int) -> Dollar: 8 | """通貨変換""" 9 | return Dollar(self._amount * multiplier) 10 | -------------------------------------------------------------------------------- /chapter-01/moneys/dollar.py: -------------------------------------------------------------------------------- 1 | """dollar通貨""" 2 | 3 | class Dollar: 4 | """ドル通貨を表します。""" 5 | def __init__(self, amount: int): 6 | """初期化""" 7 | self.amount = amount 8 | 9 | def times(self, multiplier: int): 10 | """通貨変換""" 11 | self.amount *= multiplier 12 | -------------------------------------------------------------------------------- /chapter-14/moneys/exchanger.py: -------------------------------------------------------------------------------- 1 | """通貨変換インターフェイス""" 2 | from abc import ABCMeta, abstractmethod 3 | 4 | class CurrencyExchanger(metaclass=ABCMeta): 5 | """通貨変換の機能を提供します。""" 6 | 7 | @abstractmethod 8 | def rate(self, fromcurr: str, tocurr: str) -> int: 9 | """通貨の換算率を取得します。""" 10 | pass 11 | -------------------------------------------------------------------------------- /chapter-15/moneys/exchanger.py: -------------------------------------------------------------------------------- 1 | """通貨変換インターフェイス""" 2 | from abc import ABCMeta, abstractmethod 3 | 4 | class CurrencyExchanger(metaclass=ABCMeta): 5 | """通貨変換の機能を提供します。""" 6 | 7 | @abstractmethod 8 | def rate(self, fromcurr: str, tocurr: str) -> int: 9 | """通貨の換算率を取得します。""" 10 | pass 11 | -------------------------------------------------------------------------------- /chapter-16/moneys/exchanger.py: -------------------------------------------------------------------------------- 1 | """通貨変換インターフェイス""" 2 | from abc import ABCMeta, abstractmethod 3 | 4 | class CurrencyExchanger(metaclass=ABCMeta): 5 | """通貨変換の機能を提供します。""" 6 | 7 | @abstractmethod 8 | def rate(self, fromcurr: str, tocurr: str) -> int: 9 | """通貨の換算率を取得します。""" 10 | pass 11 | -------------------------------------------------------------------------------- /chapter-06/moneys/money.py: -------------------------------------------------------------------------------- 1 | """通貨基底クラス""" 2 | 3 | class Money(object): 4 | """通貨の継承元""" 5 | def __init__(self, amount: int) -> None: 6 | """初期化""" 7 | self._amount = amount 8 | 9 | def __eq__(self, other: "Money") -> bool: 10 | """override eq""" 11 | return self._amount == other._amount 12 | -------------------------------------------------------------------------------- /chapter-01/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys import dollar 4 | 5 | class MoneyTest(ut.TestCase): 6 | """Moneyクラスのテスト""" 7 | def test_multiplication(self): 8 | """テスト""" 9 | five = dollar.Dollar(5) 10 | five.times(2) 11 | self.assertEqual(10, five.amount, "amount expected 10") 12 | -------------------------------------------------------------------------------- /chapter-02/moneys/dollar.py: -------------------------------------------------------------------------------- 1 | """dollar通貨""" 2 | from __future__ import annotations 3 | 4 | class Dollar: 5 | """ドル通貨を表します。""" 6 | def __init__(self, amount: int) -> None: 7 | """初期化""" 8 | self.amount = amount 9 | 10 | def times(self, multiplier: int) -> Dollar: 11 | """通貨変換""" 12 | return Dollar(self.amount * multiplier) 13 | -------------------------------------------------------------------------------- /chapter-09/moneys/franc.py: -------------------------------------------------------------------------------- 1 | """fran通貨""" 2 | from .money import Money 3 | 4 | class Franc(Money): 5 | """フラン通貨を表します。""" 6 | 7 | def __init__(self, amount: int) -> None: 8 | """初期化""" 9 | Money.__init__(self, amount, "CHF") 10 | 11 | def times(self, multiplier: int) -> Money: 12 | """通貨変換""" 13 | return Money.franc(self._amount * multiplier) 14 | -------------------------------------------------------------------------------- /chapter-09/moneys/dollar.py: -------------------------------------------------------------------------------- 1 | """dollar通貨""" 2 | from .money import Money 3 | 4 | class Dollar(Money): 5 | """ドル通貨を表します。""" 6 | 7 | def __init__(self, amount: int) -> None: 8 | """初期化""" 9 | Money.__init__(self, amount, "USD") 10 | 11 | def times(self, multiplier: int) -> Money: 12 | """通貨変換""" 13 | return Money.dollar(self._amount * multiplier) 14 | -------------------------------------------------------------------------------- /chapter-07/moneys/money.py: -------------------------------------------------------------------------------- 1 | """通貨基底クラス""" 2 | 3 | class Money(object): 4 | """通貨の継承元""" 5 | def __init__(self, amount: int) -> None: 6 | """初期化""" 7 | self._amount = amount 8 | 9 | def __eq__(self, other: "Money") -> bool: 10 | """override eq""" 11 | return (self._amount == other._amount) and \ 12 | (self.__class__.__name__ == other.__class__.__name__) 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-tdd 2 | 『テスト駆動開発』に出てくるサンプルを、Pythonで実装したものです。 3 | 4 | ## versions 5 | 6 | |種類|バージョン| 7 | |---|---| 8 | |Python|3.9.2| 9 | |Visual Studio Code|1.54.3| 10 | |Pylance|2021.3.2| 11 | 12 | ## ルール 13 | このソースでは、Pythonの「型アノテーション」を用いるようにします。また、`Pylance`で警告が出ないようにしています。 14 | 15 | ## テストの実行 16 | それぞれのchapterのディレクトリに移動し、以下のコマンドを実行します。 17 | 18 | ```python 19 | python -m unittest tests/moneytests.py 20 | ``` 21 | -------------------------------------------------------------------------------- /chapter-02/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys import dollar 4 | 5 | class MoneyTest(ut.TestCase): 6 | """Moneyクラスのテスト""" 7 | def test_multiplication(self): 8 | """テスト""" 9 | five = dollar.Dollar(5) 10 | product = five.times(2) 11 | self.assertEqual(10, product.amount, "5 * 2") 12 | product = five.times(3) 13 | self.assertEqual(15, product.amount, "5 * 3") 14 | -------------------------------------------------------------------------------- /chapter-05/moneys/franc.py: -------------------------------------------------------------------------------- 1 | """fran通貨""" 2 | from __future__ import annotations 3 | 4 | class Franc: 5 | """フラン通貨を表します。""" 6 | def __init__(self, amount: int) -> None: 7 | self._amount = amount 8 | 9 | def __eq__(self, other: Franc) -> bool: 10 | """override eq""" 11 | return self._amount == other._amount 12 | 13 | def times(self, multiplier: int) -> Franc: 14 | """通貨変換""" 15 | return Franc(self._amount * multiplier) 16 | -------------------------------------------------------------------------------- /chapter-04/moneys/dollar.py: -------------------------------------------------------------------------------- 1 | """dollar通貨""" 2 | from __future__ import annotations 3 | 4 | class Dollar: 5 | """ドル通貨を表します。""" 6 | def __init__(self, amount: int) -> None: 7 | self._amount = amount 8 | 9 | def __eq__(self, other: Dollar) -> bool: 10 | """override eq""" 11 | return self._amount == other._amount 12 | 13 | def times(self, multiplier: int) -> Dollar: 14 | """通貨変換""" 15 | return Dollar(self._amount * multiplier) 16 | -------------------------------------------------------------------------------- /chapter-05/moneys/dollar.py: -------------------------------------------------------------------------------- 1 | """dollar通貨""" 2 | from __future__ import annotations 3 | 4 | class Dollar: 5 | """ドル通貨を表します。""" 6 | def __init__(self, amount: int) -> None: 7 | self._amount = amount 8 | 9 | def __eq__(self, other: Dollar) -> bool: 10 | """override eq""" 11 | return self._amount == other._amount 12 | 13 | def times(self, multiplier: int) -> Dollar: 14 | """通貨変換""" 15 | return Dollar(self._amount * multiplier) 16 | -------------------------------------------------------------------------------- /chapter-03/moneys/dollar.py: -------------------------------------------------------------------------------- 1 | """dollar通貨""" 2 | from __future__ import annotations 3 | 4 | class Dollar: 5 | """ドル通貨を表します。""" 6 | def __init__(self, amount: int) -> None: 7 | """初期化""" 8 | self.amount = amount 9 | 10 | def __eq__(self, other: Dollar) -> bool: 11 | """override eq""" 12 | return self.amount == other.amount 13 | 14 | def times(self, multiplier: int) -> Dollar: 15 | """通貨変換""" 16 | return Dollar(self.amount * multiplier) 17 | -------------------------------------------------------------------------------- /chapter-11/README.md: -------------------------------------------------------------------------------- 1 | ## 第11章 不要になったら消す 2 | 3 | ### ファクトリの変更 4 | 5 | * money.py 6 | 7 | ```python 8 | @staticmethod 9 | def dollar(amount: int) -> "Money": 10 | """ドルを作成して、返す""" 11 | from .dollar import Dollar 12 | return Money(amount, "USD") 13 | 14 | @staticmethod 15 | def franc(amount: int) -> "Money": 16 | """フランを作成して、返す""" 17 | from .franc import Franc 18 | return Money(amount, "CHF") 19 | ``` 20 | 21 | このようにすると、`pylint`が「不要なimportがある」と言ってくるので、消す。 22 | 23 | 全て本の通りに消す。 -------------------------------------------------------------------------------- /chapter-04/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys.dollar import Dollar 4 | 5 | class MoneyTest(ut.TestCase): 6 | """Moneyクラスのテスト""" 7 | def test_multiplication(self): 8 | """テスト""" 9 | five = Dollar(5) 10 | self.assertEqual(Dollar(10), five.times(2), "5 * 2") 11 | self.assertEqual(Dollar(15), five.times(3), "5 * 3") 12 | 13 | def test_equality(self): 14 | """同一性テスト""" 15 | self.assertTrue(Dollar(5) == Dollar(5), "$5 == $5") 16 | self.assertFalse(Dollar(5) == Dollar(6), "$5 != $6") 17 | -------------------------------------------------------------------------------- /chapter-03/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys import dollar 4 | 5 | class MoneyTest(ut.TestCase): 6 | """Moneyクラスのテスト""" 7 | def test_multiplication(self): 8 | """テスト""" 9 | five = dollar.Dollar(5) 10 | product = five.times(2) 11 | self.assertEqual(10, product.amount, "5 * 2") 12 | product = five.times(3) 13 | self.assertEqual(15, product.amount, "5 * 3") 14 | 15 | def test_equality(self): 16 | """同一性テスト""" 17 | self.assertTrue(dollar.Dollar(5) == dollar.Dollar(5), "$5 == $5") 18 | self.assertFalse(dollar.Dollar(5) == dollar.Dollar(6), "$5 != $6") 19 | -------------------------------------------------------------------------------- /chapter-13/moneys/total.py: -------------------------------------------------------------------------------- 1 | """金額合計""" 2 | from .money import Expression, Money 3 | 4 | class Total(Expression): 5 | """金額の合計を表します。""" 6 | 7 | def __init__(self, augend: Money, addend: Money) -> None: 8 | """初期化""" 9 | self._augend = augend 10 | self._addend = addend 11 | 12 | def augend(self) -> Money: 13 | """被加算数""" 14 | return self._augend 15 | 16 | def addend(self) -> Money: 17 | """加算数""" 18 | return self._addend 19 | 20 | def reduce(self, currency: str) -> Money: 21 | """式を単純な形に変形する""" 22 | amount = self.augend().amount() + self.addend().amount() 23 | return Money(amount, currency) 24 | -------------------------------------------------------------------------------- /chapter-14/moneys/total.py: -------------------------------------------------------------------------------- 1 | """金額合計""" 2 | from .money import Expression, Money 3 | from .exchanger import CurrencyExchanger 4 | 5 | class Total(Expression): 6 | """金額の合計を表します。""" 7 | 8 | def __init__(self, augend: Money, addend: Money) -> None: 9 | """初期化""" 10 | self._augend = augend 11 | self._addend = addend 12 | 13 | def augend(self) -> Money: 14 | """被加算数""" 15 | return self._augend 16 | 17 | def addend(self) -> Money: 18 | """加算数""" 19 | return self._addend 20 | 21 | def reduce(self, bank: CurrencyExchanger, currency: str) -> Money: 22 | """式を単純な形に変形する""" 23 | amount = self.augend().amount() + self.addend().amount() 24 | return Money(amount, currency) 25 | -------------------------------------------------------------------------------- /chapter-07/README.md: -------------------------------------------------------------------------------- 1 | ## 第7章 疑念をテストに翻訳する 2 | 3 | ### ドルとフランの同一性 4 | ``` 5 | F.. 6 | ====================================================================== 7 | FAIL: test_equality (tests.moneytests.MoneyTest) 8 | 同一性テスト 9 | ---------------------------------------------------------------------- 10 | Traceback (most recent call last): 11 | File "C:\dev\python\tdd\tests\moneytests.py", line 20, in test_equality 12 | self.assertFalse(Franc(5) == Dollar(5), "f5 != $5") 13 | AssertionError: True is not false : f5 != $5 14 | 15 | ---------------------------------------------------------------------- 16 | Ran 3 tests in 0.000s 17 | 18 | FAILED (failures=1) 19 | ``` 20 | 21 | 同一ではない、というチェックをしたが、同一と判定。 22 | `Money`クラスでそう実装しているから。 23 | 24 | ### クラス名の取得 25 | ```python 26 | classname = self.__class__.__name__ 27 | ``` 28 | で取得できる。 29 | `self`の部分は、オブジェクトであればよい -------------------------------------------------------------------------------- /chapter-05/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys.dollar import Dollar 4 | from moneys.franc import Franc 5 | 6 | class MoneyTest(ut.TestCase): 7 | """Moneyクラスのテスト""" 8 | def test_multiplication(self): 9 | """テスト""" 10 | five = Dollar(5) 11 | self.assertEqual(Dollar(10), five.times(2), "5 * 2") 12 | self.assertEqual(Dollar(15), five.times(3), "5 * 3") 13 | 14 | def test_equality(self): 15 | """同一性テスト""" 16 | self.assertTrue(Dollar(5) == Dollar(5), "$5 == $5") 17 | self.assertFalse(Dollar(5) == Dollar(6), "$5 != $6") 18 | 19 | def test_franc_multiplication(self): 20 | """フランの計算""" 21 | five = Franc(5) 22 | self.assertEqual(Franc(10), five.times(2), "f10 == f10") 23 | self.assertEqual(Franc(15), five.times(3), "f15 == f15") 24 | -------------------------------------------------------------------------------- /chapter-06/README.md: -------------------------------------------------------------------------------- 1 | ## 第6章 テスト不足に気付いたら 2 | 3 | ### 継承 4 | Pythonでも、Javaと同じような継承は可能。 5 | 6 | ```python 7 | """通貨基底クラス""" 8 | 9 | class Money(object): 10 | """通貨の継承元""" 11 | pass 12 | ``` 13 | 14 | よく考えたら、独自クラスは`object`を継承すべきなので、そう記述する 15 | 16 | ### 親クラスの参照 17 | `Dollar`が`Money`を継承するようにする。 18 | 19 | * Dollarクラス 20 | 21 | ```python 22 | from .money import Money 23 | 24 | class Dollar(Money): 25 | ``` 26 | 27 | 同一階層であっても、`import`句が無いと見えないので注意 28 | 29 | ### amountを親で定義する 30 | Javaのように、フィールドが無い。そこで、 31 | 32 | ```python 33 | """通貨基底クラス""" 34 | 35 | class Money(object): 36 | """通貨の継承元""" 37 | def __init__(self, amount: int) -> None: 38 | """初期化""" 39 | self._amount = amount 40 | ``` 41 | 42 | 親クラスで`__init__`を実装する。 43 | 44 | 結果として、子クラスにある`__init__`は不要となるため、削除する。 45 | 46 | `Franc`も同様の手順で修正する。 47 | 本章の目的は、`.equals`メソッドの移動であり、それは`__eq__`の移動と同義。 48 | 結果として、子クラスからは`__init__`と`__eq__`が消えた。 -------------------------------------------------------------------------------- /chapter-14/moneys/bank.py: -------------------------------------------------------------------------------- 1 | """銀行""" 2 | from typing import Dict, Tuple 3 | from .money import Expression, Money 4 | from .exchanger import CurrencyExchanger 5 | 6 | class Bank(CurrencyExchanger): 7 | """銀行を表します。""" 8 | def __init__(self): 9 | self._rates: Dict[Tuple[str, str], int] = dict() 10 | 11 | def reduce(self, source: Expression, currency: str) -> Money: 12 | """式を単純な形に変形する""" 13 | return source.reduce(self, currency) 14 | 15 | def add_rate(self, fromcurr: str, tocurr: str, rate: int) -> None: 16 | """貨幣レートの変換登録""" 17 | self._rates[(fromcurr, tocurr)] = rate 18 | 19 | def rate(self, fromcurr: str, tocurr: str) -> int: 20 | """変換率を取得""" 21 | if fromcurr == tocurr: 22 | return 1 23 | if (fromcurr, tocurr) not in self._rates.keys(): 24 | raise KeyError() 25 | return self._rates.get((fromcurr, tocurr), 0) 26 | -------------------------------------------------------------------------------- /chapter-15/moneys/bank.py: -------------------------------------------------------------------------------- 1 | """銀行""" 2 | from typing import Dict, Tuple 3 | from .money import Expression, Money 4 | from .exchanger import CurrencyExchanger 5 | 6 | class Bank(CurrencyExchanger): 7 | """銀行を表します。""" 8 | def __init__(self): 9 | self._rates: Dict[Tuple[str, str], int] = dict() 10 | 11 | def reduce(self, source: Expression, currency: str) -> Money: 12 | """式を単純な形に変形する""" 13 | return source.reduce(self, currency) 14 | 15 | def add_rate(self, fromcurr: str, tocurr: str, rate: int) -> None: 16 | """貨幣レートの変換登録""" 17 | self._rates[(fromcurr, tocurr)] = rate 18 | 19 | def rate(self, fromcurr: str, tocurr: str) -> int: 20 | """変換率を取得""" 21 | if fromcurr == tocurr: 22 | return 1 23 | if (fromcurr, tocurr) not in self._rates.keys(): 24 | raise KeyError() 25 | return self._rates.get((fromcurr, tocurr), 0) 26 | -------------------------------------------------------------------------------- /chapter-16/moneys/bank.py: -------------------------------------------------------------------------------- 1 | """銀行""" 2 | from typing import Dict, Tuple 3 | from .money import Expression, Money 4 | from .exchanger import CurrencyExchanger 5 | 6 | class Bank(CurrencyExchanger): 7 | """銀行を表します。""" 8 | def __init__(self): 9 | self._rates: Dict[Tuple[str, str], int] = dict() 10 | 11 | def reduce(self, source: Expression, currency: str) -> Money: 12 | """式を単純な形に変形する""" 13 | return source.reduce(self, currency) 14 | 15 | def add_rate(self, fromcurr: str, tocurr: str, rate: int) -> None: 16 | """貨幣レートの変換登録""" 17 | self._rates[(fromcurr, tocurr)] = rate 18 | 19 | def rate(self, fromcurr: str, tocurr: str) -> int: 20 | """変換率を取得""" 21 | if fromcurr == tocurr: 22 | return 1 23 | if (fromcurr, tocurr) not in self._rates.keys(): 24 | raise KeyError() 25 | return self._rates.get((fromcurr, tocurr), 0) 26 | -------------------------------------------------------------------------------- /chapter-11/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys.money import Money 4 | 5 | class MoneyTest(ut.TestCase): 6 | """Moneyクラスのテスト""" 7 | def test_multiplication(self): 8 | """金額を指定倍する""" 9 | five: Money = Money.dollar(5) 10 | self.assertEqual(Money.dollar(10), five.times(2), "5 * 2") 11 | self.assertEqual(Money.dollar(15), five.times(3), "5 * 3") 12 | 13 | def test_equality(self): 14 | """同一性テスト""" 15 | self.assertTrue(Money.dollar(5) == Money.dollar(5), "$5 == $5") 16 | self.assertFalse(Money.dollar(5) == Money.dollar(6), "$5 != $6") 17 | self.assertFalse(Money.franc(5) == Money.dollar(5), "f5 != $5") 18 | 19 | def test_currency(self): 20 | """通貨単位のテスト""" 21 | self.assertEqual("USD", Money.dollar(1).currency(), "Dollar Unit") 22 | self.assertEqual("CHF", Money.franc(1).currency(), "Franc Unit") 23 | -------------------------------------------------------------------------------- /chapter-15/moneys/total.py: -------------------------------------------------------------------------------- 1 | """金額合計""" 2 | from .money import Expression, Money 3 | from .exchanger import CurrencyExchanger 4 | 5 | class Total(Expression): 6 | """金額の合計を表します。""" 7 | 8 | def __init__(self, augend: Expression, addend: Expression) -> None: 9 | """初期化""" 10 | self._augend = augend 11 | self._addend = addend 12 | 13 | def augend(self) -> Expression: 14 | """被加算数""" 15 | return self._augend 16 | 17 | def addend(self) -> Expression: 18 | """加算数""" 19 | return self._addend 20 | 21 | def reduce(self, bank: CurrencyExchanger, currency: str) -> Money: 22 | """式を単純な形に変形する""" 23 | amount = self.augend().reduce(bank, currency).amount() + \ 24 | self.addend().reduce(bank, currency).amount() 25 | return Money(amount, currency) 26 | 27 | def plus(self, addend: Expression) -> Expression: 28 | """加算""" 29 | raise NotImplementedError() 30 | -------------------------------------------------------------------------------- /chapter-08/moneys/money.py: -------------------------------------------------------------------------------- 1 | """通貨基底クラス""" 2 | from __future__ import annotations 3 | from abc import ABCMeta, abstractmethod 4 | 5 | class Money(metaclass=ABCMeta): 6 | """通貨の継承元""" 7 | 8 | def __init__(self, amount: int) -> None: 9 | """初期化""" 10 | self._amount = amount 11 | 12 | @abstractmethod 13 | def times(self, multiplier: int) -> Money: 14 | """金額を指定倍して返す""" 15 | pass 16 | 17 | def __eq__(self, other: Money) -> bool: 18 | """override eq""" 19 | return (self._amount == other._amount) and \ 20 | (self.__class__.__name__ == other.__class__.__name__) 21 | 22 | @staticmethod 23 | def dollar(amount: int) -> Money: 24 | """ドルを作成して、返す""" 25 | from .dollar import Dollar 26 | return Dollar(amount) 27 | 28 | @staticmethod 29 | def franc(amount: int) -> Money: 30 | """フランを作成して、返す""" 31 | from .franc import Franc 32 | return Franc(amount) 33 | -------------------------------------------------------------------------------- /chapter-06/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys.dollar import Dollar 4 | from moneys.franc import Franc 5 | 6 | class MoneyTest(ut.TestCase): 7 | """Moneyクラスのテスト""" 8 | def test_multiplication(self): 9 | """テスト""" 10 | five = Dollar(5) 11 | self.assertEqual(Dollar(10), five.times(2), "5 * 2") 12 | self.assertEqual(Dollar(15), five.times(3), "5 * 3") 13 | 14 | def test_equality(self): 15 | """同一性テスト""" 16 | self.assertTrue(Dollar(5) == Dollar(5), "$5 == $5") 17 | self.assertFalse(Dollar(5) == Dollar(6), "$5 != $6") 18 | self.assertTrue(Franc(5) == Franc(5), "f5 == f5") 19 | self.assertFalse(Franc(5) == Franc(6), "f5 != f6") 20 | 21 | def test_franc_multiplication(self): 22 | """フランの計算""" 23 | five = Franc(5) 24 | self.assertEqual(Franc(10), five.times(2), "f10 == f10") 25 | self.assertEqual(Franc(15), five.times(3), "f15 == f15") 26 | -------------------------------------------------------------------------------- /chapter-16/README.md: -------------------------------------------------------------------------------- 1 | ## 第16章 将来の読み手を考えたテスト 2 | 3 | ### テスト追加 4 | テスト自体は本と一緒 5 | 6 | 結果は以下の通り 7 | 8 | ``` 9 | ..........E. 10 | ====================================================================== 11 | ERROR: test_total_times (tests.moneytests.MoneyTest) 12 | moneyの倍 13 | ---------------------------------------------------------------------- 14 | Traceback (most recent call last): 15 | File "C:\dev\python\tdd\tests\moneytests.py", line 91, in test_total_times 16 | total = Total(five_bucks, ten_francs).times(2) 17 | AttributeError: 'Total' object has no attribute 'times' 18 | 19 | ---------------------------------------------------------------------- 20 | Ran 12 tests in 0.003s 21 | 22 | FAILED (errors=1) 23 | ``` 24 | 25 | `Total.times`が無い。 26 | 27 | 追加も本と一緒で良い。 28 | publicに引き上げる部分は不要 29 | 30 | ### Moneyを返す 31 | 一応書いた。 32 | 33 | ```python 34 | def test_plus_samecurrency_money(self): 35 | """加算でMoneyを返す""" 36 | total = Money.dollar(1).plus(Money.dollar(1)) 37 | self.assertIsInstance(total, Money) 38 | ``` -------------------------------------------------------------------------------- /chapter-02/README.md: -------------------------------------------------------------------------------- 1 | ## 第2章 明白な実装 2 | 3 | ### Dollarの副作用 4 | 5 | * 初回のエラー 6 | 7 | ``` 8 | ====================================================================== 9 | ERROR: test_multiplication (tests.moneytests.MoneyTest) 10 | テスト 11 | ---------------------------------------------------------------------- 12 | Traceback (most recent call last): 13 | File "C:\dev\python\tdd\tests\moneytests.py", line 11, in test_multiplication 14 | self.assertEqual(10, product.amount, "5 * 2") 15 | AttributeError: 'NoneType' object has no attribute 'amount' 16 | 17 | ---------------------------------------------------------------------- 18 | Ran 1 test in 0.001s 19 | 20 | FAILED (errors=1) 21 | ``` 22 | 23 | ### 本との違い 24 | Pythonでは、戻り値の型は推論して動くことから、**nullを返す**必要が無い。 25 | 26 | そもそも、型指定自体必要ないが、Python3.5以降の型アノテーションを利用し、戻り値型を指定している。 27 | 28 | ただし、 29 | 30 | ```python 31 | def times(self, multiplier: int) -> Dollar: 32 | ``` 33 | 34 | と定義を記載すると、`Dollarが見つからない`とエラーになる。 35 | (Dollarの定義が終わっていないため、らしい) 36 | 37 | 前方参照も無理そうなので、ここでは文字列で指定して、見て分かるようにしている。 -------------------------------------------------------------------------------- /chapter-09/README.md: -------------------------------------------------------------------------------- 1 | ## 第9章 歩幅の調整 2 | 3 | ### currencyの追加 4 | `Money`に、抽象メソッド`currency`を追加する。 5 | 6 | ```python 7 | @abstractmethod 8 | def currency(self) -> str: 9 | """通貨単位""" 10 | pass 11 | ``` 12 | 13 | 第8章で見せられなかったが、`ABCMeta`を使うと、子クラスで`@abstractmethod`相当のメソッドを実装していないと、下記のようにエラーになる。 14 | 15 | ``` 16 | EEEE 17 | ====================================================================== 18 | ERROR: test_currency (tests.moneytests.MoneyTest) 19 | 通貨単位のテスト 20 | ---------------------------------------------------------------------- 21 | Traceback (most recent call last): 22 | File "C:\dev\python\tdd\tests\moneytests.py", line 29, in test_currency 23 | self.assertEqual("USD", Money.dollar(1).currency(), "Dollar Unit") 24 | File "C:\dev\python\tdd\moneys\money.py", line 30, in dollar 25 | return Dollar(amount) 26 | TypeError: Can't instantiate abstract class Dollar with abstract methods currency 27 | ``` 28 | 29 | 本章は、本にあるJavaの例とやることはほとんど変わらない。 30 | 31 | ただ、現状、本のコンストラクタ定義と、Pythonでのコンストラクタ定義が異なる。 32 | (6章から違っていたけど、スルーしていた) -------------------------------------------------------------------------------- /chapter-07/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys.dollar import Dollar 4 | from moneys.franc import Franc 5 | 6 | class MoneyTest(ut.TestCase): 7 | """Moneyクラスのテスト""" 8 | def test_multiplication(self): 9 | """テスト""" 10 | five = Dollar(5) 11 | self.assertEqual(Dollar(10), five.times(2), "5 * 2") 12 | self.assertEqual(Dollar(15), five.times(3), "5 * 3") 13 | 14 | def test_equality(self): 15 | """同一性テスト""" 16 | self.assertTrue(Dollar(5) == Dollar(5), "$5 == $5") 17 | self.assertFalse(Dollar(5) == Dollar(6), "$5 != $6") 18 | self.assertTrue(Franc(5) == Franc(5), "f5 == f5") 19 | self.assertFalse(Franc(5) == Franc(6), "f5 != f6") 20 | self.assertFalse(Franc(5) == Dollar(5), "f5 != $5") 21 | 22 | def test_franc_multiplication(self): 23 | """フランの計算""" 24 | five = Franc(5) 25 | self.assertEqual(Franc(10), five.times(2), "f10 == f10") 26 | self.assertEqual(Franc(15), five.times(3), "f15 == f15") 27 | -------------------------------------------------------------------------------- /chapter-08/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys.money import Money 4 | 5 | class MoneyTest(ut.TestCase): 6 | """Moneyクラスのテスト""" 7 | def test_multiplication(self): 8 | """金額を指定倍する""" 9 | five: Money = Money.dollar(5) 10 | self.assertEqual(Money.dollar(10), five.times(2), "5 * 2") 11 | self.assertEqual(Money.dollar(15), five.times(3), "5 * 3") 12 | 13 | def test_equality(self): 14 | """同一性テスト""" 15 | self.assertTrue(Money.dollar(5) == Money.dollar(5), "$5 == $5") 16 | self.assertFalse(Money.dollar(5) == Money.dollar(6), "$5 != $6") 17 | self.assertTrue(Money.franc(5) == Money.franc(5), "f5 == f5") 18 | self.assertFalse(Money.franc(5) == Money.franc(6), "f5 != f6") 19 | self.assertFalse(Money.franc(5) == Money.dollar(5), "f5 != $5") 20 | 21 | def test_franc_multiplication(self): 22 | """フランの計算""" 23 | five = Money.franc(5) 24 | self.assertEqual(Money.franc(10), five.times(2), "f10 == f10") 25 | self.assertEqual(Money.franc(15), five.times(3), "f15 == f15") 26 | -------------------------------------------------------------------------------- /chapter-11/moneys/money.py: -------------------------------------------------------------------------------- 1 | """通貨基底クラス""" 2 | from __future__ import annotations 3 | 4 | class Money(object): 5 | """通貨の継承元""" 6 | 7 | def __init__(self, amount: int, currency: str) -> None: 8 | """初期化""" 9 | self._amount = amount 10 | self._currency = currency 11 | 12 | def times(self, multiplier: int) -> Money: 13 | """金額を指定倍して返す""" 14 | return Money(self._amount * multiplier, self.currency()) 15 | 16 | def currency(self) -> str: 17 | """通貨単位""" 18 | return self._currency 19 | 20 | def __eq__(self, other: Money) -> bool: 21 | """override eq""" 22 | return (self._amount == other._amount) and (self.currency() == other.currency()) 23 | 24 | @staticmethod 25 | def dollar(amount: int) -> Money: 26 | """ドルを作成して、返す""" 27 | return Money(amount, "USD") 28 | 29 | @staticmethod 30 | def franc(amount: int) -> Money: 31 | """フランを作成して、返す""" 32 | return Money(amount, "CHF") 33 | 34 | def __repr__(self): 35 | """クラス表現""" 36 | return str(self._amount) + " " + self._currency 37 | -------------------------------------------------------------------------------- /chapter-09/moneys/money.py: -------------------------------------------------------------------------------- 1 | """通貨基底クラス""" 2 | from __future__ import annotations 3 | from abc import ABCMeta, abstractmethod 4 | 5 | class Money(metaclass=ABCMeta): 6 | """通貨の継承元""" 7 | 8 | def __init__(self, amount: int, currency: str) -> None: 9 | """初期化""" 10 | self._amount = amount 11 | self._currency = currency 12 | 13 | @abstractmethod 14 | def times(self, multiplier: int) -> Money: 15 | """金額を指定倍して返す""" 16 | pass 17 | 18 | def currency(self) -> str: 19 | """通貨単位""" 20 | return self._currency 21 | 22 | def __eq__(self, other: Money) -> bool: 23 | """override eq""" 24 | return (self._amount == other._amount) and \ 25 | (self.__class__.__name__ == other.__class__.__name__) 26 | 27 | @staticmethod 28 | def dollar(amount: int) -> Money: 29 | """ドルを作成して、返す""" 30 | from .dollar import Dollar 31 | return Dollar(amount) 32 | 33 | @staticmethod 34 | def franc(amount: int) -> Money: 35 | """フランを作成して、返す""" 36 | from .franc import Franc 37 | return Franc(amount) 38 | -------------------------------------------------------------------------------- /chapter-16/moneys/total.py: -------------------------------------------------------------------------------- 1 | """金額合計""" 2 | from .money import Expression, Money 3 | from .exchanger import CurrencyExchanger 4 | 5 | class Total(Expression): 6 | """金額の合計を表します。""" 7 | 8 | def __init__(self, augend: Expression, addend: Expression) -> None: 9 | """初期化""" 10 | self._augend = augend 11 | self._addend = addend 12 | 13 | def augend(self) -> Expression: 14 | """被加算数""" 15 | return self._augend 16 | 17 | def addend(self) -> Expression: 18 | """加算数""" 19 | return self._addend 20 | 21 | def reduce(self, bank: CurrencyExchanger, currency: str) -> Money: 22 | """式を単純な形に変形する""" 23 | amount = self.augend().reduce(bank, currency).amount() + \ 24 | self.addend().reduce(bank, currency).amount() 25 | return Money(amount, currency) 26 | 27 | def plus(self, addend: Expression) -> Expression: 28 | """加算""" 29 | return Total(self, addend) 30 | 31 | def times(self, multiplier: int) -> Expression: 32 | """指定倍""" 33 | return Total(self.augend().times(multiplier), self.addend().times(multiplier)) 34 | -------------------------------------------------------------------------------- /chapter-12/README.md: -------------------------------------------------------------------------------- 1 | ## 第12章 設計とメタファー 2 | 3 | ### 注意事項 4 | 本では、合計の変数名を`sum`としているが、Pythonだと、標準で同名のメソッドが存在する。 5 | 安全性を考慮して、そこは変数名を変更している。 6 | 7 | また、これまで`_amount`というフィールドにしていた値を、`amount()`メソッドで取得できるようにした。 8 | 9 | ```python 10 | def amount(self) -> int: 11 | """金額""" 12 | return self._amount 13 | ``` 14 | 15 | ### plusメソッドの追加 16 | ```python 17 | def plus(self, addend: "Money") -> "Money": 18 | """加算""" 19 | return Money(self.amount() + addend.amount(), self.currency()) 20 | ``` 21 | 22 | ### Bank、Expressionの追加 23 | 24 | インターフェイス`Expression`を追加する。 25 | が、Pythonにはinterfaceが無いので、抽象クラスで代用する。 26 | 27 | ※Pythonは多重継承をサポートしているので、抽象クラスで十分代用可能 28 | 29 | * expression.py 30 | 31 | ```python 32 | """式を表す""" 33 | from abc import ABCMeta 34 | 35 | class Expression(metaclass=ABCMeta): 36 | """式(演算)を表します。""" 37 | pass 38 | ``` 39 | 40 | * bank.py 41 | 42 | ```python 43 | """銀行""" 44 | from .expression import Expression 45 | from .money import Money 46 | 47 | class Bank(object): 48 | """銀行を表します。""" 49 | def reduce(self, source: Expression, to: str) -> Money: 50 | """式を単純な形に変形する""" 51 | return Money.dollar(10) 52 | ``` -------------------------------------------------------------------------------- /chapter-10/moneys/money.py: -------------------------------------------------------------------------------- 1 | """通貨基底クラス""" 2 | from __future__ import annotations 3 | 4 | class Money(object): 5 | """通貨の継承元""" 6 | 7 | def __init__(self, amount: int, currency: str) -> None: 8 | """初期化""" 9 | self._amount = amount 10 | self._currency = currency 11 | 12 | def times(self, multiplier: int) -> Money: 13 | """金額を指定倍して返す""" 14 | return Money(self._amount * multiplier, self.currency()) 15 | 16 | def currency(self) -> str: 17 | """通貨単位""" 18 | return self._currency 19 | 20 | def __eq__(self, other: Money) -> bool: 21 | """override eq""" 22 | return (self._amount == other._amount) and (self.currency() == other.currency()) 23 | 24 | @staticmethod 25 | def dollar(amount: int) -> Money: 26 | """ドルを作成して、返す""" 27 | from .dollar import Dollar 28 | return Dollar(amount) 29 | 30 | @staticmethod 31 | def franc(amount: int) -> Money: 32 | """フランを作成して、返す""" 33 | from .franc import Franc 34 | return Franc(amount) 35 | 36 | def __repr__(self): 37 | """クラス表現""" 38 | return str(self._amount) + " " + self._currency 39 | -------------------------------------------------------------------------------- /chapter-12/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys.money import Money 4 | from moneys.bank import Bank 5 | 6 | class MoneyTest(ut.TestCase): 7 | """Moneyクラスのテスト""" 8 | def test_multiplication(self): 9 | """金額を指定倍する""" 10 | five: Money = Money.dollar(5) 11 | self.assertEqual(Money.dollar(10), five.times(2), "5 * 2") 12 | self.assertEqual(Money.dollar(15), five.times(3), "5 * 3") 13 | 14 | def test_equality(self): 15 | """同一性テスト""" 16 | self.assertTrue(Money.dollar(5) == Money.dollar(5), "$5 == $5") 17 | self.assertFalse(Money.dollar(5) == Money.dollar(6), "$5 != $6") 18 | self.assertFalse(Money.franc(5) == Money.dollar(5), "f5 != $5") 19 | 20 | def test_currency(self): 21 | """通貨単位のテスト""" 22 | self.assertEqual("USD", Money.dollar(1).currency(), "Dollar Unit") 23 | self.assertEqual("CHF", Money.franc(1).currency(), "Franc Unit") 24 | 25 | def test_simple_addition(self): 26 | """同一単位の加算""" 27 | five = Money.dollar(5) 28 | total = five.plus(five) 29 | bank = Bank() 30 | reduce = bank.reduce(total, "USD") 31 | self.assertEqual(Money.dollar(10), reduce, "equal") 32 | -------------------------------------------------------------------------------- /chapter-04/README.md: -------------------------------------------------------------------------------- 1 | ## 第4章 意図を語るテスト 2 | 3 | ### 章に入る前に 4 | テスト側のインポートで、クラスをインポートするように修正した。 5 | 6 | * 修正前 7 | ```python 8 | from moneys import dollar 9 | ``` 10 | 11 | * 修正後 12 | ```python 13 | from moneys.dollar import Dollar 14 | ``` 15 | 16 | ### インライン化 17 | 上記、インポート修正も含め、かなり見やすくなった 18 | 19 | ```python 20 | def test_multiplication(self): 21 | """テスト""" 22 | five = Dollar(5) 23 | self.assertEqual(Dollar(10), five.times(2), "5 * 2") 24 | self.assertEqual(Dollar(15), five.times(3), "5 * 3") 25 | ``` 26 | 27 | ### privateメンバーにする 28 | 29 | Pythonは、Privateメンバーという概念が**無い**。 30 | 31 | [Pythonチュートリアル:プライベート変数](https://docs.python.jp/3/tutorial/classes.html#private-variables) 32 | 33 | チュートリアルの記載に倣い、アンダースコアを付ける。 34 | 35 | ```python 36 | class Dollar: 37 | """ドル通貨を表します。""" 38 | def __init__(self, amount: int) -> None: 39 | self._amount = amount 40 | 41 | def __eq__(self, other: "Dollar") -> bool: 42 | """override eq""" 43 | return self._amount == other._amount 44 | 45 | def times(self, multiplier: int) -> "Dollar": 46 | """通貨変換""" 47 | return Dollar(self._amount * multiplier) 48 | ``` 49 | 50 | `other._amount`の部分で、pylintが警告を出すが、問題があるソースではないため、無視して良いと考えられる。 51 | Javaならアクセス可能。 52 | 53 | あえてamountを隠すなら、という実装。Pythonだとプロパティを利用したほうが良いように思う。 54 | -------------------------------------------------------------------------------- /chapter-09/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys.money import Money 4 | 5 | class MoneyTest(ut.TestCase): 6 | """Moneyクラスのテスト""" 7 | def test_multiplication(self): 8 | """金額を指定倍する""" 9 | five: Money = Money.dollar(5) 10 | self.assertEqual(Money.dollar(10), five.times(2), "5 * 2") 11 | self.assertEqual(Money.dollar(15), five.times(3), "5 * 3") 12 | 13 | def test_equality(self): 14 | """同一性テスト""" 15 | self.assertTrue(Money.dollar(5) == Money.dollar(5), "$5 == $5") 16 | self.assertFalse(Money.dollar(5) == Money.dollar(6), "$5 != $6") 17 | self.assertTrue(Money.franc(5) == Money.franc(5), "f5 == f5") 18 | self.assertFalse(Money.franc(5) == Money.franc(6), "f5 != f6") 19 | self.assertFalse(Money.franc(5) == Money.dollar(5), "f5 != $5") 20 | 21 | def test_franc_multiplication(self): 22 | """フランの計算""" 23 | five = Money.franc(5) 24 | self.assertEqual(Money.franc(10), five.times(2), "f10 == f10") 25 | self.assertEqual(Money.franc(15), five.times(3), "f15 == f15") 26 | 27 | def test_currency(self): 28 | """通貨単位のテスト""" 29 | self.assertEqual("USD", Money.dollar(1).currency(), "Dollar Unit") 30 | self.assertEqual("CHF", Money.franc(1).currency(), "Franc Unit") 31 | -------------------------------------------------------------------------------- /chapter-05/README.md: -------------------------------------------------------------------------------- 1 | ## 第5章 原則をあえて破るとき 2 | 3 | ### 解説 4 | ここでの「あえて破るとき」というのは、 5 | 6 | *本来コピペは悪だが、最終的に重複排除するから許せ* 7 | 8 | ということ。 9 | 10 | ### Francをテストに追加 11 | ``` 12 | .E. 13 | ====================================================================== 14 | ERROR: test_franc_multiplication (tests.moneytests.MoneyTest) 15 | フランの計算 16 | ---------------------------------------------------------------------- 17 | Traceback (most recent call last): 18 | File "C:\dev\python\tdd\tests\moneytests.py", line 20, in test_franc_multiplication 19 | five = Franc(5) 20 | NameError: name 'Franc' is not defined 21 | 22 | ---------------------------------------------------------------------- 23 | Ran 3 tests in 0.002s 24 | 25 | FAILED (errors=1) 26 | ``` 27 | 28 | テストには失敗する。 29 | 30 | ### Francクラスの追加 31 | ```python 32 | """fran通貨""" 33 | 34 | class Franc: 35 | """フラン通貨を表します。""" 36 | def __init__(self, amount: int) -> None: 37 | self._amount = amount 38 | 39 | def __eq__(self, other: "Franc") -> bool: 40 | """override eq""" 41 | return self._amount == other._amount 42 | 43 | def times(self, multiplier: int) -> "Franc": 44 | """通貨変換""" 45 | return Franc(self._amount * multiplier) 46 | 47 | ``` 48 | 49 | 単に、Dollarをコピーして、Francで置き換える 50 | 51 | * テスト 52 | 53 | ```python 54 | from moneys.franc import Franc 55 | ``` 56 | 57 | の一文を追加することで、テストは成功するようになる -------------------------------------------------------------------------------- /chapter-12/moneys/money.py: -------------------------------------------------------------------------------- 1 | """通貨クラス""" 2 | from __future__ import annotations 3 | from .expression import Expression 4 | 5 | class Money(Expression): 6 | """通貨""" 7 | 8 | def __init__(self, amount: int, currency: str) -> None: 9 | """初期化""" 10 | self._amount = amount 11 | self._currency = currency 12 | 13 | def times(self, multiplier: int) -> Money: 14 | """金額を指定倍して返す""" 15 | return Money(self.amount() * multiplier, self.currency()) 16 | 17 | def amount(self) -> int: 18 | """金額""" 19 | return self._amount 20 | 21 | def currency(self) -> str: 22 | """通貨単位""" 23 | return self._currency 24 | 25 | def plus(self, addend: Money) -> Expression: 26 | """加算""" 27 | return Money(self.amount() + addend.amount(), self.currency()) 28 | 29 | @staticmethod 30 | def dollar(amount: int) -> Money: 31 | """ドルを作成して、返す""" 32 | return Money(amount, "USD") 33 | 34 | @staticmethod 35 | def franc(amount: int) -> Money: 36 | """フランを作成して、返す""" 37 | return Money(amount, "CHF") 38 | 39 | def __eq__(self, other: Money) -> bool: 40 | """override eq""" 41 | return (self.amount() == other.amount()) and (self.currency() == other.currency()) 42 | 43 | def __repr__(self): 44 | """クラス表現""" 45 | return str(self.amount()) + " " + self.currency() 46 | -------------------------------------------------------------------------------- /chapter-13/README.md: -------------------------------------------------------------------------------- 1 | ## 第13章 実装を導くテスト 2 | 3 | 前章から引き続き、`Sum`は`Total`として表現する。 4 | 5 | ### キャスト 6 | Pythonは型指定が無いので、本で指定されるようなアップキャストは、あまり意味がない。 7 | 8 | ### Sumクラス(Totalクラス) 9 | Pythonはインターフェイスが無いため、抽象クラスで代用するのは、12章で述べた通り。 10 | 11 | ```python 12 | """金額合計""" 13 | from. expression import Expression 14 | from .money import Money 15 | 16 | class Total(Expression): 17 | """金額の合計を表します。""" 18 | 19 | def __init__(self, augend: Money, addend: Money) -> None: 20 | """初期化""" 21 | self._augend = augend 22 | self._addend = addend 23 | 24 | def augend(self) -> Money: 25 | """被加算数""" 26 | return self._augend 27 | 28 | def addend(self) -> Money: 29 | """加算数""" 30 | return self._addend 31 | ``` 32 | 33 | ### reduce関連 34 | 35 | #### Expression 36 | `money.py`とファイルを分けると、 `from .money import Money`という記述が循環参照となるため、 `money.py`内に定義している 37 | 38 | #### `total.py` 39 | 40 | ```python 41 | def reduce(self, currency: str) -> Money: 42 | """式を単純な形に変形する""" 43 | amount = self.augend().amount() + self.addend().amount() 44 | return Money(amount, currency) 45 | ``` 46 | 47 | #### `bank.py` 48 | 49 | ```python 50 | def reduce(self, source: Expression, currency: str) -> Money: 51 | """式を単純な形に変形する""" 52 | return source.reduce(currency) 53 | ``` 54 | 55 | 本には、 56 | 57 | > キーワード引数を扱えるようなプログラミング言語であれば、差異を際立たせることが可能 58 | 59 | と書いてあるが、よくわからないので、本と同じ実装になっている。 -------------------------------------------------------------------------------- /chapter-10/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys.money import Money 4 | from moneys.franc import Franc 5 | 6 | class MoneyTest(ut.TestCase): 7 | """Moneyクラスのテスト""" 8 | def test_multiplication(self): 9 | """金額を指定倍する""" 10 | five: Money = Money.dollar(5) 11 | self.assertEqual(Money.dollar(10), five.times(2), "5 * 2") 12 | self.assertEqual(Money.dollar(15), five.times(3), "5 * 3") 13 | 14 | def test_equality(self): 15 | """同一性テスト""" 16 | self.assertTrue(Money.dollar(5) == Money.dollar(5), "$5 == $5") 17 | self.assertFalse(Money.dollar(5) == Money.dollar(6), "$5 != $6") 18 | self.assertTrue(Money.franc(5) == Money.franc(5), "f5 == f5") 19 | self.assertFalse(Money.franc(5) == Money.franc(6), "f5 != f6") 20 | self.assertFalse(Money.franc(5) == Money.dollar(5), "f5 != $5") 21 | 22 | def test_franc_multiplication(self): 23 | """フランの計算""" 24 | five = Money.franc(5) 25 | self.assertEqual(Money.franc(10), five.times(2), "f10 == f10") 26 | self.assertEqual(Money.franc(15), five.times(3), "f15 == f15") 27 | 28 | def test_currency(self): 29 | """通貨単位のテスト""" 30 | self.assertEqual("USD", Money.dollar(1).currency(), "Dollar Unit") 31 | self.assertEqual("CHF", Money.franc(1).currency(), "Franc Unit") 32 | 33 | def test_differentclass_equality(self): 34 | """異なる通貨の同一性テスト""" 35 | self.assertTrue(Money(10, "CHF") == Franc(10), "Money = Franc") 36 | -------------------------------------------------------------------------------- /chapter-13/moneys/money.py: -------------------------------------------------------------------------------- 1 | """通貨クラス""" 2 | from __future__ import annotations 3 | from abc import ABCMeta, abstractmethod 4 | 5 | class Expression(metaclass=ABCMeta): 6 | """式(演算)を表します。""" 7 | 8 | @abstractmethod 9 | def reduce(self, currency: str) -> Money: 10 | """式を単純な形に変形する""" 11 | pass 12 | 13 | class Money(Expression): 14 | """通貨""" 15 | 16 | def __init__(self, amount: int, currency: str) -> None: 17 | """初期化""" 18 | self._amount = amount 19 | self._currency = currency 20 | 21 | def times(self, multiplier: int) -> Money: 22 | """金額を指定倍して返す""" 23 | return Money(self.amount() * multiplier, self.currency()) 24 | 25 | def amount(self) -> int: 26 | """金額""" 27 | return self._amount 28 | 29 | def currency(self) -> str: 30 | """通貨単位""" 31 | return self._currency 32 | 33 | def plus(self, addend: Money) -> Expression: 34 | """加算""" 35 | from .total import Total 36 | return Total(self, addend) 37 | 38 | def reduce(self, currency: str) -> Money: 39 | return self 40 | 41 | @staticmethod 42 | def dollar(amount: int) -> Money: 43 | """ドルを作成して、返す""" 44 | return Money(amount, "USD") 45 | 46 | @staticmethod 47 | def franc(amount: int) -> Money: 48 | """フランを作成して、返す""" 49 | return Money(amount, "CHF") 50 | 51 | def __eq__(self, other: Money) -> bool: 52 | """override eq""" 53 | return (self.amount() == other.amount()) and (self.currency() == other.currency()) 54 | 55 | def __repr__(self): 56 | """クラス表現""" 57 | return str(self.amount()) + " " + self.currency() 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /chapter-14/moneys/money.py: -------------------------------------------------------------------------------- 1 | """通貨クラス""" 2 | from __future__ import annotations 3 | from abc import ABCMeta, abstractmethod 4 | from .exchanger import CurrencyExchanger 5 | 6 | 7 | class Expression(metaclass=ABCMeta): 8 | """式(演算)を表します。""" 9 | 10 | @abstractmethod 11 | def reduce(self, bank: CurrencyExchanger, currency: str) -> Money: 12 | """式を単純な形に変形する""" 13 | pass 14 | 15 | 16 | class Money(Expression): 17 | """通貨""" 18 | 19 | def __init__(self, amount: int, currency: str) -> None: 20 | """初期化""" 21 | self._amount = amount 22 | self._currency = currency 23 | 24 | def times(self, multiplier: int) -> Money: 25 | """金額を指定倍して返す""" 26 | return Money(self.amount() * multiplier, self.currency()) 27 | 28 | def amount(self) -> int: 29 | """金額""" 30 | return self._amount 31 | 32 | def currency(self) -> str: 33 | """通貨単位""" 34 | return self._currency 35 | 36 | def plus(self, addend: Money) -> Expression: 37 | """加算""" 38 | from .total import Total 39 | return Total(self, addend) 40 | 41 | def reduce(self, bank: CurrencyExchanger, currency: str) -> Money: 42 | rate = bank.rate(self.currency(), currency) 43 | return Money(self.amount() / rate, currency) 44 | 45 | @staticmethod 46 | def dollar(amount: int) -> Money: 47 | """ドルを作成して、返す""" 48 | return Money(amount, "USD") 49 | 50 | @staticmethod 51 | def franc(amount: int) -> Money: 52 | """フランを作成して、返す""" 53 | return Money(amount, "CHF") 54 | 55 | def __eq__(self, other: Money) -> bool: 56 | """override eq""" 57 | return (self.amount() == other.amount()) and (self.currency() == other.currency()) 58 | 59 | def __repr__(self): 60 | """クラス表現""" 61 | return str(self.amount()) + " " + self.currency() 62 | -------------------------------------------------------------------------------- /chapter-15/moneys/money.py: -------------------------------------------------------------------------------- 1 | """通貨クラス""" 2 | from __future__ import annotations 3 | from abc import ABCMeta, abstractmethod 4 | from .exchanger import CurrencyExchanger 5 | 6 | class Expression(metaclass=ABCMeta): 7 | """式(演算)を表します。""" 8 | 9 | @abstractmethod 10 | def plus(self, addend: Expression) -> Expression: 11 | """加算""" 12 | pass 13 | 14 | @abstractmethod 15 | def reduce(self, bank: CurrencyExchanger, currency: str) -> Money: 16 | """式を単純な形に変形する""" 17 | pass 18 | 19 | 20 | class Money(Expression): 21 | """通貨""" 22 | 23 | def __init__(self, amount: int, currency: str) -> None: 24 | """初期化""" 25 | self._amount = amount 26 | self._currency = currency 27 | 28 | def times(self, multiplier: int) -> Expression: 29 | """金額を指定倍して返す""" 30 | return Money(self.amount() * multiplier, self.currency()) 31 | 32 | def amount(self) -> int: 33 | """金額""" 34 | return self._amount 35 | 36 | def currency(self) -> str: 37 | """通貨単位""" 38 | return self._currency 39 | 40 | def plus(self, addend: Expression) -> Expression: 41 | """加算""" 42 | from .total import Total 43 | return Total(self, addend) 44 | 45 | def reduce(self, bank: CurrencyExchanger, currency: str) -> Money: 46 | rate = bank.rate(self.currency(), currency) 47 | return Money(self.amount() // rate, currency) 48 | 49 | @staticmethod 50 | def dollar(amount: int) -> Money: 51 | """ドルを作成して、返す""" 52 | return Money(amount, "USD") 53 | 54 | @staticmethod 55 | def franc(amount: int) -> Money: 56 | """フランを作成して、返す""" 57 | return Money(amount, "CHF") 58 | 59 | def __eq__(self, other: Money) -> bool: 60 | """override eq""" 61 | return (self.amount() == other.amount()) and (self.currency() == other.currency()) 62 | 63 | def __repr__(self): 64 | """クラス表現""" 65 | return str(self.amount()) + " " + self.currency() 66 | -------------------------------------------------------------------------------- /chapter-13/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys.money import Money 4 | from moneys.bank import Bank 5 | from moneys.total import Total 6 | 7 | class MoneyTest(ut.TestCase): 8 | """Moneyクラスのテスト""" 9 | def test_multiplication(self): 10 | """金額を指定倍する""" 11 | five: Money = Money.dollar(5) 12 | self.assertEqual(Money.dollar(10), five.times(2), "5 * 2") 13 | self.assertEqual(Money.dollar(15), five.times(3), "5 * 3") 14 | 15 | def test_equality(self): 16 | """同一性テスト""" 17 | self.assertTrue(Money.dollar(5) == Money.dollar(5), "$5 == $5") 18 | self.assertFalse(Money.dollar(5) == Money.dollar(6), "$5 != $6") 19 | self.assertFalse(Money.franc(5) == Money.dollar(5), "f5 != $5") 20 | 21 | def test_currency(self): 22 | """通貨単位のテスト""" 23 | self.assertEqual("USD", Money.dollar(1).currency(), "Dollar Unit") 24 | self.assertEqual("CHF", Money.franc(1).currency(), "Franc Unit") 25 | 26 | def test_simple_addition(self): 27 | """同一単位の加算""" 28 | five = Money.dollar(5) 29 | total = five.plus(five) 30 | bank = Bank() 31 | reduced = bank.reduce(total, "USD") 32 | self.assertEqual(Money.dollar(10), reduced, "equal") 33 | 34 | def test_plus_returnsum(self): 35 | """合計を返す場合のテスト""" 36 | five = Money.dollar(5) 37 | result = five.plus(five) 38 | total: Total = result 39 | self.assertEqual(five, total.augend()) 40 | self.assertEqual(five, total.addend()) 41 | 42 | def test_reducesum(self): 43 | """合計のテスト""" 44 | total = Total(Money.dollar(3), Money.dollar(4)) 45 | bank = Bank() 46 | result = bank.reduce(total, "USD") 47 | self.assertEqual(Money.dollar(7), result) 48 | 49 | def test_reduce_money(self): 50 | """Moneyのテスト""" 51 | bank = Bank() 52 | result = bank.reduce(Money.dollar(1), "USD") 53 | self.assertEqual(Money.dollar(1), result, "Money as expression") 54 | -------------------------------------------------------------------------------- /chapter-16/moneys/money.py: -------------------------------------------------------------------------------- 1 | """通貨クラス""" 2 | from __future__ import annotations 3 | from abc import ABCMeta, abstractmethod 4 | from .exchanger import CurrencyExchanger 5 | 6 | class Expression(metaclass=ABCMeta): 7 | """式(演算)を表します。""" 8 | 9 | @abstractmethod 10 | def plus(self, addend: Expression) -> "Expression": 11 | """加算""" 12 | pass 13 | 14 | @abstractmethod 15 | def reduce(self, bank: CurrencyExchanger, currency: str) -> Money: 16 | """式を単純な形に変形する""" 17 | pass 18 | 19 | @abstractmethod 20 | def times(self, multiplier: int) -> Expression: 21 | """指定倍""" 22 | pass 23 | 24 | class Money(Expression): 25 | """通貨""" 26 | 27 | def __init__(self, amount: int, currency: str) -> None: 28 | """初期化""" 29 | self._amount = amount 30 | self._currency = currency 31 | 32 | def times(self, multiplier: int) -> Expression: 33 | """金額を指定倍して返す""" 34 | return Money(self.amount() * multiplier, self.currency()) 35 | 36 | def amount(self) -> int: 37 | """金額""" 38 | return self._amount 39 | 40 | def currency(self) -> str: 41 | """通貨単位""" 42 | return self._currency 43 | 44 | def plus(self, addend: Expression) -> Expression: 45 | """加算""" 46 | from .total import Total 47 | return Total(self, addend) 48 | 49 | def reduce(self, bank: CurrencyExchanger, currency: str) -> Money: 50 | rate = bank.rate(self.currency(), currency) 51 | return Money(self.amount() // rate, currency) 52 | 53 | @staticmethod 54 | def dollar(amount: int) -> Money: 55 | """ドルを作成して、返す""" 56 | return Money(amount, "USD") 57 | 58 | @staticmethod 59 | def franc(amount: int) -> Money: 60 | """フランを作成して、返す""" 61 | return Money(amount, "CHF") 62 | 63 | def __eq__(self, other: Money) -> bool: 64 | """override eq""" 65 | return (self.amount() == other.amount()) and (self.currency() == other.currency()) 66 | 67 | def __repr__(self): 68 | """クラス表現""" 69 | return str(self.amount()) + " " + self.currency() 70 | -------------------------------------------------------------------------------- /chapter-03/README.md: -------------------------------------------------------------------------------- 1 | ## 第3章 三角測量 2 | 3 | ### 失敗例 その1 4 | 5 | * ソース 6 | 7 | ```python 8 | def test_equality(self): 9 | """同一性テスト""" 10 | self.assertTrue(dollar.Dollar(5) == dollar.Dollar(5)) 11 | ``` 12 | 13 | * 結果 14 | 15 | ``` 16 | F. 17 | ====================================================================== 18 | FAIL: test_equality (tests.moneytests.MoneyTest) 19 | 同一性テスト 20 | ---------------------------------------------------------------------- 21 | Traceback (most recent call last): 22 | File "C:\dev\python\tdd\tests\moneytests.py", line 17, in test_equality 23 | self.assertTrue(dollar.Dollar(5) == dollar.Dollar(5)) 24 | AssertionError: False is not true 25 | 26 | ---------------------------------------------------------------------- 27 | Ran 2 tests in 0.001s 28 | 29 | FAILED (failures=1) 30 | ``` 31 | 32 | 失敗する。 33 | Pythonには、Javaの`equals`相当のメソッドが無いので、単に`==`で比較する。 34 | 35 | ### 失敗例 その2 36 | 37 | Pythonで、Javaの`equals`と似た動きをさせるなら、`__eq__`を実装する 38 | 39 | ```python 40 | def __eq__(self, other) -> bool: 41 | """override eq""" 42 | return True 43 | ``` 44 | 45 | ``` 46 | .. 47 | ---------------------------------------------------------------------- 48 | Ran 2 tests in 0.000s 49 | 50 | OK 51 | ``` 52 | 53 | 成功はするが、テストを書き直すと 54 | 55 | * テスト 56 | ```python 57 | def test_equality(self): 58 | """同一性テスト""" 59 | self.assertTrue(dollar.Dollar(5) == dollar.Dollar(5), "$5 == $5") 60 | self.assertFalse(dollar.Dollar(5) == dollar.Dollar(6), "$5 != $6") 61 | ``` 62 | 63 | ``` 64 | F. 65 | ====================================================================== 66 | FAIL: test_equality (tests.moneytests.MoneyTest) 67 | 同一性テスト 68 | ---------------------------------------------------------------------- 69 | Traceback (most recent call last): 70 | File "C:\dev\python\tdd\tests\moneytests.py", line 18, in test_equality 71 | self.assertFalse(dollar.Dollar(5) == dollar.Dollar(6), "$5 != $6") 72 | AssertionError: True is not false : $5 != $6 73 | 74 | ---------------------------------------------------------------------- 75 | Ran 2 tests in 0.001s 76 | 77 | FAILED (failures=1) 78 | ``` 79 | 80 | 失敗する。 81 | 82 | ### 成功例 83 | 84 | ```python 85 | def __eq__(self, other: "Dollar") -> bool: 86 | """override eq""" 87 | return self.amount == other.amount 88 | ``` 89 | 90 | ``` 91 | .. 92 | ---------------------------------------------------------------------- 93 | Ran 2 tests in 0.001s 94 | 95 | OK 96 | ``` 97 | 98 | 引数に、文字列でDollarを指定すると、予測変換が可能になる。(Visual Studio Codeの場合) -------------------------------------------------------------------------------- /chapter-14/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys.money import Money 4 | from moneys.bank import Bank 5 | from moneys.total import Total 6 | 7 | class MoneyTest(ut.TestCase): 8 | """Moneyクラスのテスト""" 9 | def test_multiplication(self): 10 | """金額を指定倍する""" 11 | five: Money = Money.dollar(5) 12 | self.assertEqual(Money.dollar(10), five.times(2), "5 * 2") 13 | self.assertEqual(Money.dollar(15), five.times(3), "5 * 3") 14 | 15 | def test_equality(self): 16 | """同一性テスト""" 17 | self.assertTrue(Money.dollar(5) == Money.dollar(5), "$5 == $5") 18 | self.assertFalse(Money.dollar(5) == Money.dollar(6), "$5 != $6") 19 | self.assertFalse(Money.franc(5) == Money.dollar(5), "f5 != $5") 20 | 21 | def test_currency(self): 22 | """通貨単位のテスト""" 23 | self.assertEqual("USD", Money.dollar(1).currency(), "Dollar Unit") 24 | self.assertEqual("CHF", Money.franc(1).currency(), "Franc Unit") 25 | 26 | def test_simple_addition(self): 27 | """同一単位の加算""" 28 | five = Money.dollar(5) 29 | total = five.plus(five) 30 | bank = Bank() 31 | reduced = bank.reduce(total, "USD") 32 | self.assertEqual(Money.dollar(10), reduced, "equal") 33 | 34 | def test_plus_returnsum(self): 35 | """合計を返す場合のテスト""" 36 | five = Money.dollar(5) 37 | result = five.plus(five) 38 | total: Total = result 39 | self.assertEqual(five, total.augend()) 40 | self.assertEqual(five, total.addend()) 41 | 42 | def test_reducesum(self): 43 | """合計のテスト""" 44 | total = Total(Money.dollar(3), Money.dollar(4)) 45 | bank = Bank() 46 | result = bank.reduce(total, "USD") 47 | self.assertEqual(Money.dollar(7), result) 48 | 49 | def test_reduce_money(self): 50 | """Moneyのテスト""" 51 | bank = Bank() 52 | result = bank.reduce(Money.dollar(1), "USD") 53 | self.assertEqual(Money.dollar(1), result, "Money as expression") 54 | 55 | def test_reducemoney_diff_currency(self): 56 | """別の貨幣へ変換""" 57 | bank = Bank() 58 | bank.add_rate("CHF", "USD", 2) 59 | result = bank.reduce(Money.franc(2), "USD") 60 | self.assertEqual(Money.dollar(1), result) 61 | 62 | def test_identity_rate(self): 63 | """同一貨幣""" 64 | self.assertEqual(1, Bank().rate("USD", "USD")) 65 | 66 | def test_rate_notfound(self): 67 | """変換レートが存在しない""" 68 | bank = Bank() 69 | with self.assertRaises(KeyError): 70 | bank.rate("USD", "FRN") 71 | -------------------------------------------------------------------------------- /chapter-15/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys.money import Money 4 | from moneys.bank import Bank 5 | from moneys.total import Total 6 | 7 | class MoneyTest(ut.TestCase): 8 | """Moneyクラスのテスト""" 9 | def test_multiplication(self): 10 | """金額を指定倍する""" 11 | five: Money = Money.dollar(5) 12 | self.assertEqual(Money.dollar(10), five.times(2), "5 * 2") 13 | self.assertEqual(Money.dollar(15), five.times(3), "5 * 3") 14 | 15 | def test_equality(self): 16 | """同一性テスト""" 17 | self.assertTrue(Money.dollar(5) == Money.dollar(5), "$5 == $5") 18 | self.assertFalse(Money.dollar(5) == Money.dollar(6), "$5 != $6") 19 | self.assertFalse(Money.franc(5) == Money.dollar(5), "f5 != $5") 20 | 21 | def test_currency(self): 22 | """通貨単位のテスト""" 23 | self.assertEqual("USD", Money.dollar(1).currency(), "Dollar Unit") 24 | self.assertEqual("CHF", Money.franc(1).currency(), "Franc Unit") 25 | 26 | def test_simple_addition(self): 27 | """同一単位の加算""" 28 | five = Money.dollar(5) 29 | total = five.plus(five) 30 | bank = Bank() 31 | reduced = bank.reduce(total, "USD") 32 | self.assertEqual(Money.dollar(10), reduced, "equal") 33 | 34 | def test_plus_returnsum(self): 35 | """合計を返す場合のテスト""" 36 | five = Money.dollar(5) 37 | result = five.plus(five) 38 | total: Total = result 39 | self.assertEqual(five, total.augend()) 40 | self.assertEqual(five, total.addend()) 41 | 42 | def test_reducesum(self): 43 | """合計のテスト""" 44 | total = Total(Money.dollar(3), Money.dollar(4)) 45 | bank = Bank() 46 | result = bank.reduce(total, "USD") 47 | self.assertEqual(Money.dollar(7), result) 48 | 49 | def test_reduce_money(self): 50 | """Moneyのテスト""" 51 | bank = Bank() 52 | result = bank.reduce(Money.dollar(1), "USD") 53 | self.assertEqual(Money.dollar(1), result, "Money as expression") 54 | 55 | def test_reducemoney_diff_currency(self): 56 | """別の貨幣へ変換""" 57 | bank = Bank() 58 | bank.add_rate("CHF", "USD", 2) 59 | result = bank.reduce(Money.franc(2), "USD") 60 | self.assertEqual(Money.dollar(1), result) 61 | 62 | def test_identity_rate(self): 63 | """同一貨幣""" 64 | self.assertEqual(1, Bank().rate("USD", "USD")) 65 | 66 | def test_rate_notfound(self): 67 | """変換レートが存在しない""" 68 | bank = Bank() 69 | with self.assertRaises(KeyError): 70 | bank.rate("USD", "FRN") 71 | 72 | def test_mixed_addition(self): 73 | """異なる通貨の加算""" 74 | five_bucks = Money.dollar(5) 75 | ten_francs = Money.franc(10) 76 | bank = Bank() 77 | bank.add_rate("CHF", "USD", 2) 78 | result = bank.reduce(five_bucks.plus(ten_francs), "USD") 79 | self.assertEqual(Money.dollar(10), result) 80 | -------------------------------------------------------------------------------- /chapter-08/README.md: -------------------------------------------------------------------------------- 1 | ## 第8章 実装を隠す 2 | 3 | ### timesの戻り値型変更 4 | Pythonでは、意味がない。 5 | このサンプル上、型アノテーションを使って書いているが、これはコンパイラには無視される。 6 | 7 | ### ファクトリメソッド 8 | ``` 9 | ..E 10 | ====================================================================== 11 | ERROR: test_multiplication (tests.moneytests.MoneyTest) 12 | 金額を指定倍する 13 | ---------------------------------------------------------------------- 14 | Traceback (most recent call last): 15 | File "C:\dev\python\tdd\tests\moneytests.py", line 11, in test_multiplication 16 | five = Money.dollar(5) 17 | AttributeError: type object 'Money' has no attribute 'dollar' 18 | 19 | ---------------------------------------------------------------------- 20 | Ran 3 tests in 0.002s 21 | 22 | FAILED (errors=1) 23 | ``` 24 | 25 | 存在しない型を作るので、いったん失敗 26 | 27 | ### moneyにimportを追加 28 | 29 | * money.py 30 | 31 | ```python 32 | """通貨基底クラス""" 33 | from .dollar import Dollar 34 | 35 | class Money(object): 36 | """通貨の継承元""" 37 | def __init__(self, amount: int) -> None: 38 | """初期化""" 39 | self._amount = amount 40 | 41 | def __eq__(self, other: "Money") -> bool: 42 | """override eq""" 43 | return (self._amount == other._amount) and \ 44 | (self.__class__.__name__ == other.__class__.__name__) 45 | 46 | @staticmethod 47 | def dollar(amount: int) -> Dollar: 48 | """ドルを作成して、返す""" 49 | return Dollar(amount) 50 | 51 | ``` 52 | 53 | staticメソッドとして、`Dollar`の作成処理を追加する 54 | Pythonでstaticメソッドを利用する場合、`@staticmethod`と付ける 55 | 56 | ``` 57 | E 58 | ====================================================================== 59 | ERROR: moneytests (unittest.loader._FailedTest) 60 | ---------------------------------------------------------------------- 61 | ImportError: Failed to import test module: moneytests 62 | Traceback (most recent call last): 63 | File "C:\Users\maste\AppData\Local\Programs\Python\Python36\lib\unittest\loader.py", line 153, in loadTestsFromName 64 | module = __import__(module_name) 65 | File "C:\dev\python\tdd\tests\moneytests.py", line 3, in 66 | from moneys.dollar import Dollar 67 | File "C:\dev\python\tdd\moneys\dollar.py", line 2, in 68 | from .money import Money 69 | File "C:\dev\python\tdd\moneys\money.py", line 2, in 70 | from .dollar import Dollar 71 | ImportError: cannot import name 'Dollar' 72 | 73 | 74 | ---------------------------------------------------------------------- 75 | Ran 1 test in 0.000s 76 | 77 | FAILED (errors=1) 78 | ``` 79 | 80 | これではエラーになる。 81 | 循環参照か? 82 | 83 | [Qiitaの参考にしたリンク](https://qiita.com/puriketu99/items/a1347bf5200f095e486e) 84 | 85 | 上記に倣い、`dollar`メソッドを修正する。 86 | 87 | ```python 88 | @staticmethod 89 | def dollar(amount: int) -> "Dollar": 90 | """ドルを作成して、返す""" 91 | from .dollar import Dollar 92 | return Dollar(amount) 93 | ``` 94 | 95 | 型アノテーションの`Dollar`は、モジュールのimportからDollarを消したため、参照できない。 96 | そのため、文字列に変更する 97 | 98 | ### Moneyにtimesを移動 99 | 本では、 100 | 101 | > コンパイラが親切にMoney型にはtimesメソッドが定義されていないと教えてくれる。 102 | 103 | とあるが、Pythonは教えてくれない。(ダックタイピング) 104 | 105 | * moneytest.py 106 | 107 | ```python 108 | def test_multiplication(self): 109 | """金額を指定倍する""" 110 | five: Money = Money.dollar(5) 111 | self.assertEqual(Dollar(10), five.times(2), "5 * 2") 112 | self.assertEqual(Dollar(15), five.times(3), "5 * 3") 113 | ``` 114 | 115 | という風に、`five`に型指定を入れても、Intellisense(予測変換)では出なくなるが、テストは通る。 116 | 117 | Pythonは、仕組み上抽象メソッドは無いので、`ABCMeta`を使う -------------------------------------------------------------------------------- /chapter-16/tests/moneytests.py: -------------------------------------------------------------------------------- 1 | """金額テスト""" 2 | import unittest as ut 3 | from moneys.money import Money 4 | from moneys.bank import Bank 5 | from moneys.total import Total 6 | 7 | class MoneyTest(ut.TestCase): 8 | """Moneyクラスのテスト""" 9 | def test_multiplication(self): 10 | """金額を指定倍する""" 11 | five: Money = Money.dollar(5) 12 | self.assertEqual(Money.dollar(10), five.times(2), "5 * 2") 13 | self.assertEqual(Money.dollar(15), five.times(3), "5 * 3") 14 | 15 | def test_equality(self): 16 | """同一性テスト""" 17 | self.assertTrue(Money.dollar(5) == Money.dollar(5), "$5 == $5") 18 | self.assertFalse(Money.dollar(5) == Money.dollar(6), "$5 != $6") 19 | self.assertFalse(Money.franc(5) == Money.dollar(5), "f5 != $5") 20 | 21 | def test_currency(self): 22 | """通貨単位のテスト""" 23 | self.assertEqual("USD", Money.dollar(1).currency(), "Dollar Unit") 24 | self.assertEqual("CHF", Money.franc(1).currency(), "Franc Unit") 25 | 26 | def test_simple_addition(self): 27 | """同一単位の加算""" 28 | five = Money.dollar(5) 29 | total = five.plus(five) 30 | bank = Bank() 31 | reduced = bank.reduce(total, "USD") 32 | self.assertEqual(Money.dollar(10), reduced, "equal") 33 | 34 | def test_plus_returnsum(self): 35 | """合計を返す場合のテスト""" 36 | five = Money.dollar(5) 37 | result = five.plus(five) 38 | total: Total = result 39 | self.assertEqual(five, total.augend()) 40 | self.assertEqual(five, total.addend()) 41 | 42 | def test_reducesum(self): 43 | """合計のテスト""" 44 | total = Total(Money.dollar(3), Money.dollar(4)) 45 | bank = Bank() 46 | result = bank.reduce(total, "USD") 47 | self.assertEqual(Money.dollar(7), result) 48 | 49 | def test_reduce_money(self): 50 | """Moneyのテスト""" 51 | bank = Bank() 52 | result = bank.reduce(Money.dollar(1), "USD") 53 | self.assertEqual(Money.dollar(1), result, "Money as expression") 54 | 55 | def test_reducemoney_diff_currency(self): 56 | """別の貨幣へ変換""" 57 | bank = Bank() 58 | bank.add_rate("CHF", "USD", 2) 59 | result = bank.reduce(Money.franc(2), "USD") 60 | self.assertEqual(Money.dollar(1), result) 61 | 62 | def test_identity_rate(self): 63 | """同一貨幣""" 64 | self.assertEqual(1, Bank().rate("USD", "USD")) 65 | 66 | def test_rate_notfound(self): 67 | """変換レートが存在しない""" 68 | bank = Bank() 69 | with self.assertRaises(KeyError): 70 | bank.rate("USD", "FRN") 71 | 72 | def test_mixed_addition(self): 73 | """異なる通貨の加算""" 74 | five_bucks = Money.dollar(5) 75 | ten_francs = Money.franc(10) 76 | bank = Bank() 77 | bank.add_rate("CHF", "USD", 2) 78 | result = bank.reduce(five_bucks.plus(ten_francs), "USD") 79 | self.assertEqual(Money.dollar(10), result) 80 | 81 | def test_totalplus_money(self): 82 | """合計の加算""" 83 | five_bucks = Money.dollar(5) 84 | ten_francs = Money.franc(10) 85 | bank = Bank() 86 | bank.add_rate("CHF", "USD", 2) 87 | total = Total(five_bucks, ten_francs).plus(five_bucks) 88 | result = bank.reduce(total, "USD") 89 | self.assertEqual(Money.dollar(15), result) 90 | 91 | def test_total_times(self): 92 | """moneyの倍""" 93 | five_bucks = Money.dollar(5) 94 | ten_francs = Money.franc(10) 95 | bank = Bank() 96 | bank.add_rate("CHF", "USD", 2) 97 | total = Total(five_bucks, ten_francs).times(2) 98 | result = bank.reduce(total, "USD") 99 | self.assertEqual(Money.dollar(20), result) 100 | -------------------------------------------------------------------------------- /chapter-14/README.md: -------------------------------------------------------------------------------- 1 | ## 第14章 学習用テストと回帰テスト 2 | 3 | ### テスト 4 | 5 | * moneytests.py 6 | 7 | ```python 8 | def test_reducemoney_diff_currency(self): 9 | """別の貨幣へ変換""" 10 | bank = Bank() 11 | bank.add_rate("CHF", "USD", 2) 12 | result = bank.reduce(Money.franc(2), "USD") 13 | self.assertEqual(Money.dollar(1), result) 14 | ``` 15 | 16 | `pylint`が発行する警告に対応して、メソッド名(長すぎる)を修正 17 | 18 | ### Moneyの実装 19 | * money.py 20 | ```python 21 | def reduce(self, currency: str) -> "Money": 22 | rate = 2 if (self.currency() == "CHF" and currency == "USD") else 1 23 | return Money(self.amount() / rate, currency) 24 | ``` 25 | 26 | Python の三項演算子は、以下の通り。 27 | ```python 28 | [trueで返す値] if ([条件式]) else [falseで返す値] 29 | ``` 30 | 31 | ### bankの引数追加 32 | `Sum`(ここでは`Total`)クラスの`reduce`に、`Bank`の引数を追加した。 33 | 本では触れられていないが、これはインターフェイスの整合性のために追加されており、ここでは使う必要はない。 34 | 35 | ```python 36 | def reduce(self, bank: Bank, currency: str) -> Money: 37 | """式を単純な形に変形する""" 38 | amount = self.augend().amount() + self.addend().amount() 39 | return Money(amount, currency) 40 | ``` 41 | 42 | 問題となるのは、相互参照。 43 | 循環参照が発生してしまう。 44 | 45 | ※ただし、引数の型指定を止めれば発生しないと思われる。 46 | 47 | Bank ←→ Expression 48 | 49 | が相互参照となるため、本とは異なるが、`Bank.rate`のインターフェイスを用意する。 50 | `CurrencyExchanger`インターフェイスを作成。 51 | 52 | ```python 53 | """通貨変換インターフェイス""" 54 | from abc import ABCMeta, abstractmethod 55 | 56 | class CurrencyExchanger(metaclass=ABCMeta): 57 | """通貨変換の機能を提供します。""" 58 | 59 | @abstractmethod 60 | def rate(self, fromcurr: str, tocurr: str) -> int: 61 | """通貨の換算率を取得します。""" 62 | pass 63 | ``` 64 | 65 | ### Pairクラス 66 | Pairクラスを、辞書のキーとして使おうとしている。 67 | 68 | Pythonでは、辞書のキーとしてタプル(もしくは`frozenset`)を使ったほうが良い。 69 | ここでは、タプルを使うこととする。 70 | →Pairクラスは登場しない 71 | 72 | ```python 73 | class Bank(CurrencyExchanger): 74 | """銀行を表します。""" 75 | def __init__(self): 76 | self._rates = dict() 77 | 78 | def reduce(self, source: Expression, currency: str) -> Money: 79 | """式を単純な形に変形する""" 80 | return source.reduce(self, currency) 81 | 82 | def add_rate(self, fromcurr: str, tocurr: str, rate: int) -> None: 83 | """貨幣レートの変換登録""" 84 | self._rates[(fromcurr, tocurr)] = rate 85 | 86 | def rate(self, fromcurr: str, tocurr: str) -> int: 87 | """変換率を取得""" 88 | return self._rates.get((fromcurr, tocurr)) 89 | ``` 90 | 91 | 92 | ### 修正後テスト 93 | ``` 94 | ....E... 95 | ====================================================================== 96 | ERROR: test_reduce_money (tests.moneytests.MoneyTest) 97 | Moneyのテスト 98 | ---------------------------------------------------------------------- 99 | Traceback (most recent call last): 100 | File "C:\dev\python\tdd\tests\moneytests.py", line 52, in test_reduce_money 101 | result = bank.reduce(Money.dollar(1), "USD") 102 | File "C:\dev\python\tdd\moneys\bank.py", line 13, in reduce 103 | return source.reduce(self, currency) 104 | File "C:\dev\python\tdd\moneys\money.py", line 32, in reduce 105 | return Money(self.amount() / rate, currency) 106 | TypeError: unsupported operand type(s) for /: 'int' and 'NoneType' 107 | 108 | ---------------------------------------------------------------------- 109 | Ran 8 tests in 0.003s 110 | 111 | FAILED (errors=1) 112 | ``` 113 | 114 | どうやら、`rate`の戻り値がおかしい模様(本と同じ) 115 | 116 | 回帰テストも、予想通り失敗する 117 | 118 | ``` 119 | FAIL: test_identity_rate (tests.moneytests.MoneyTest) 120 | 同一貨幣 121 | ---------------------------------------------------------------------- 122 | Traceback (most recent call last): 123 | File "C:\dev\python\tdd\tests\moneytests.py", line 64, in test_identity_rate 124 | self.assertEqual(1, Bank().rate("USD", "USD")) 125 | AssertionError: 1 != None 126 | 127 | ---------------------------------------------------------------------- 128 | Ran 9 tests in 0.016s 129 | 130 | FAILED (failures=1, errors=1) 131 | ``` 132 | 133 | `bank.py`の修正で直る。 134 | 135 | ```python 136 | def rate(self, fromcurr: str, tocurr: str) -> int: 137 | """変換率を取得""" 138 | if fromcurr == tocurr: 139 | return 1 140 | return self._rates.get((fromcurr, tocurr)) 141 | ``` -------------------------------------------------------------------------------- /chapter-15/README.md: -------------------------------------------------------------------------------- 1 | ## 第15章 テスト任せとコンパイラ任せ 2 | 3 | ### テストの追加 4 | 5 | ```python 6 | def test_mixed_addition(self): 7 | """異なる通貨の加算""" 8 | five_bucks = Money.dollar(5) 9 | ten_francs = Money.franc(10) 10 | bank = Bank() 11 | bank.add_rate("CHF", "USD", 2) 12 | result = bank.reduce(five_bucks.plus(ten_francs), "USD") 13 | self.assertEqual(Money.dollar(10), result) 14 | ``` 15 | 16 | 本では、 17 | 18 | > 大量のコンパイルエラーが出る 19 | 20 | とあるが、一つも出ない。 21 | `five_bucks`が、型指定なしで`Money`と扱われているため。 22 | 23 | テストは、以下の通り失敗する。 24 | 25 | ``` 26 | ...F...... 27 | ====================================================================== 28 | FAIL: test_mixed_addition (tests.moneytests.MoneyTest) 29 | 異なる通貨の加算 30 | ---------------------------------------------------------------------- 31 | Traceback (most recent call last): 32 | File "C:\dev\python\tdd\tests\moneytests.py", line 73, in test_mixed_addition 33 | self.assertEqual(Money.dollar(10), result) 34 | AssertionError: 10 USD != 15 USD 35 | 36 | ---------------------------------------------------------------------- 37 | Ran 10 tests in 0.003s 38 | 39 | FAILED (failures=1) 40 | ``` 41 | 42 | ### テストを通す 43 | ```python 44 | def reduce(self, bank: CurrencyExchanger, currency: str) -> Money: 45 | """式を単純な形に変形する""" 46 | amount = self.augend().reduce(bank, currency).amount() + \ 47 | self.addend().reduce(bank, currency).amount() 48 | return Money(amount, currency) 49 | ``` 50 | 51 | 1行が長すぎると、`pylint`の警告に該当するため、二行に分ける 52 | 53 | ### plusをExpressionに追加 54 | 55 | * expression.py 56 | 57 | ```python 58 | @abstractmethod 59 | def plus(self, addend: "Expression") -> "Expression": 60 | """加算""" 61 | pass 62 | ``` 63 | 64 | 自身は参照できないため、型は文字列。 65 | 66 | いっぱいエラーが出る。 67 | ``` 68 | ...E.E..EE 69 | ====================================================================== 70 | ERROR: test_mixed_addition (tests.moneytests.MoneyTest) 71 | 異なる通貨の加算 72 | ---------------------------------------------------------------------- 73 | Traceback (most recent call last): 74 | File "C:\dev\python\tdd\tests\moneytests.py", line 72, in test_mixed_addition 75 | result = bank.reduce(five_bucks.plus(ten_francs), "USD") 76 | File "C:\dev\python\tdd\moneys\money.py", line 28, in plus 77 | return Total(self, addend) 78 | TypeError: Can't instantiate abstract class Total with abstract methods plus 79 | 80 | ====================================================================== 81 | ERROR: test_plus_returnsum (tests.moneytests.MoneyTest) 82 | 合計を返す場合のテスト 83 | ---------------------------------------------------------------------- 84 | Traceback (most recent call last): 85 | File "C:\dev\python\tdd\tests\moneytests.py", line 37, in test_plus_returnsum 86 | result = five.plus(five) 87 | File "C:\dev\python\tdd\moneys\money.py", line 28, in plus 88 | return Total(self, addend) 89 | TypeError: Can't instantiate abstract class Total with abstract methods plus 90 | 91 | ====================================================================== 92 | ERROR: test_reducesum (tests.moneytests.MoneyTest) 93 | 合計のテスト 94 | ---------------------------------------------------------------------- 95 | Traceback (most recent call last): 96 | File "C:\dev\python\tdd\tests\moneytests.py", line 44, in test_reducesum 97 | total = Total(Money.dollar(3), Money.dollar(4)) 98 | TypeError: Can't instantiate abstract class Total with abstract methods plus 99 | 100 | ====================================================================== 101 | ERROR: test_simple_addition (tests.moneytests.MoneyTest) 102 | 同一単位の加算 103 | ---------------------------------------------------------------------- 104 | Traceback (most recent call last): 105 | File "C:\dev\python\tdd\tests\moneytests.py", line 29, in test_simple_addition 106 | total = five.plus(five) 107 | File "C:\dev\python\tdd\moneys\money.py", line 28, in plus 108 | return Total(self, addend) 109 | TypeError: Can't instantiate abstract class Total with abstract methods plus 110 | 111 | ---------------------------------------------------------------------- 112 | Ran 10 tests in 0.004s 113 | 114 | FAILED (errors=4) 115 | ``` 116 | 117 | MoneyはPlusを持っていないため、エラーになる 118 | 本の通り、空実装を追加してエラーを消す -------------------------------------------------------------------------------- /chapter-10/README.md: -------------------------------------------------------------------------------- 1 | ## 第10章 テストに聞いてみる 2 | 3 | ### timesメソッドでMoneyを返す 4 | * franc.py 5 | 6 | ```python 7 | def times(self, multiplier: int) -> Money: 8 | """通貨変換""" 9 | return Money(self._amount * multiplier, self.currency) 10 | ``` 11 | 12 | エラーになる。`ABCMeta`を適用したことで、抽象クラス扱いであるため。 13 | 14 | ``` 15 | ..E. 16 | ====================================================================== 17 | ERROR: test_franc_multiplication (tests.moneytests.MoneyTest) 18 | フランの計算 19 | ---------------------------------------------------------------------- 20 | Traceback (most recent call last): 21 | File "C:\dev\python\tdd\tests\moneytests.py", line 24, in test_franc_multiplication 22 | self.assertEqual(Money.franc(10), five.times(2), "f10 == f10") 23 | File "C:\dev\python\tdd\moneys\franc.py", line 13, in times 24 | return Money(self._amount * multiplier) 25 | TypeError: Can't instantiate abstract class Money with abstract methods times 26 | 27 | ---------------------------------------------------------------------- 28 | Ran 4 tests in 0.008s 29 | 30 | FAILED (errors=1) 31 | ``` 32 | 33 | ### Moneyの抽象クラスを止める 34 | 35 | 継承として指定されていた、`metaclass=ABCMeta`を外し、`object`にする 36 | 37 | ```python 38 | class Money(object): 39 | """通貨の継承元""" 40 | ``` 41 | 42 | テストはやはりエラーになる。 43 | 44 | ``` 45 | ..F. 46 | ====================================================================== 47 | FAIL: test_franc_multiplication (tests.moneytests.MoneyTest) 48 | フランの計算 49 | ---------------------------------------------------------------------- 50 | Traceback (most recent call last): 51 | File "C:\dev\python\tdd\tests\moneytests.py", line 24, in test_franc_multiplication 52 | self.assertEqual(Money.franc(10), five.times(2), "f10 == f10") 53 | AssertionError: != : f10 == f10 54 | 55 | ---------------------------------------------------------------------- 56 | Ran 4 tests in 0.007s 57 | 58 | FAILED (failures=1) 59 | ``` 60 | 61 | ### toStringの実装 62 | Javaの`toString`と同様の機能として、Pythoには`__repr__`がある。 63 | `Money`クラスで実装する。 64 | 65 | ```python 66 | def __repr__(self): 67 | """クラス表現""" 68 | return str(self._amount) + " " + self._currency 69 | ``` 70 | 71 | エラーには変わりないが、メッセージが変わる 72 | 73 | ``` 74 | ..F. 75 | ====================================================================== 76 | FAIL: test_franc_multiplication (tests.moneytests.MoneyTest) 77 | フランの計算 78 | ---------------------------------------------------------------------- 79 | Traceback (most recent call last): 80 | File "C:\dev\python\tdd\tests\moneytests.py", line 24, in test_franc_multiplication 81 | self.assertEqual(Money.franc(10), five.times(2), "f10 == f10") 82 | AssertionError: 10 CHF != : f10 == f10 83 | 84 | ---------------------------------------------------------------------- 85 | Ran 4 tests in 0.006s 86 | 87 | FAILED (failures=1) 88 | ``` 89 | 90 | ただ、Javaと違って、親クラスの`toString`が引き継がれるわけでは無い模様 91 | 92 | ### FrancとMoneyの比較 93 | テストを追加する。 94 | 95 | ```python 96 | def test_differentclass_equality(self): 97 | """異なる通貨の同一性テスト""" 98 | self.assertTrue(Money(10, "CHF") == Franc(10), "Money = Franc") 99 | ``` 100 | 101 | 失敗する。 102 | 103 | ``` 104 | .F... 105 | ====================================================================== 106 | FAIL: test_differentclass_equality (tests.moneytests.MoneyTest) 107 | 異なる通貨の同一性テスト 108 | ---------------------------------------------------------------------- 109 | Traceback (most recent call last): 110 | File "C:\dev\python\tdd\tests\moneytests.py", line 35, in test_differentclass_equality 111 | self.assertTrue(Money(10, "CHF") == Franc(10), "Money = Franc") 112 | AssertionError: False is not true : Money = Franc 113 | 114 | ---------------------------------------------------------------------- 115 | Ran 5 tests in 0.003s 116 | 117 | FAILED (failures=1) 118 | ``` 119 | 120 | ### `__eq__`の修正 121 | 122 | ```python 123 | def __eq__(self, other: "Money") -> bool: 124 | """override eq""" 125 | return (self._amount == other._amount) and (self.currency() == other.currency()) 126 | ``` 127 | 128 | `currency`を見るよう、修正する 129 | 130 | 131 | ### timesの移動 132 | 本に従って、`times`を`Money`に移動させる。 133 | 134 | その結果、`Money`クラスは 135 | 136 | ```python 137 | """通貨基底クラス""" 138 | 139 | class Money(object): 140 | """通貨の継承元""" 141 | ``` 142 | 143 | のように、`ABCMeta`と`abstractmethod`のインポートが不要となった。(抽象クラスではなくなった) --------------------------------------------------------------------------------