書籍「テスト駆動開発」の**第I部「多国通貨」**で取り上げられている実例は、JAVAベースのため、自分の理解を深めるためにも、Pythonで同等のプラクティスに挑んでみました。
ちなみに、テスト駆動開発プラクティスのコードは、以下のGithubに保管しています。
https://github.com/ttsubo/study_of_test_driven_development/tree/master/python
■ テスト駆動開発の進め方
書籍「テスト駆動開発」で提唱しているテスト駆動開発の進め方は、次の通りです。
- 小さいテストを1つ書く。
- すべてのテストを実行し、1つ失敗することを確認する。
- 小さい変更を行う。
- 再びテストを実行し、すべて成功することを確認する。
- リファクタリングを行い、重複を除去する。
今回のプラクティスでも、極力、この進め方に追従していきます。
■ 多国通貨を通じて、テスト駆動開発を経験する
実現すべき要件は、以下の2つ。
- 通貨の異なる2つの金額を足し、通貨間の為替レートに基づいて換算された金額を得る。
- 金額(通貨単位あたりの額)に数値(通貨単位数)を掛け、金額を得る。
第1章: 仮実装 (Multi-Currency Money)
まずは、金額(通貨単位あたりの額)に数値(通貨単位数)を掛け、金額を得る。
という仕組みを目指します。
具体的には、$5*2=$10
が成立するように、コード化を進めていきます。
(1) まずは、小さいテストを1つ書く。
$5*2=$10
が成立することを確認するテストを作成します。
from testtools import TestCase
from example.dollar import Dollar
class MoneyTest(TestCase):
def testMultiplication(self):
five = Dollar(5)
five.times(2)
self.assertEqual(10, five.amount)
(2) テスト対象のコードを準備しておく
Dollar
クラスをロードするだけのコードを配備しておきます。
class Dollar:
pass
(3) テストを実行し、1つ失敗することを確認する。
Dollar
クラスのオブジェクト化できませんので、テストは失敗します。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testMultiplication FAILED [100%]
================================== FAILURES ===================================
___________________________ MoneyTest.testMultiplication ______________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
File "/Users/ttsubo/source/study_of_test_driven_development/tests/test_money.py", line 6, in testMultiplication
five = Dollar(5)
TypeError: Dollar() takes no arguments
(4) 小さい変更を行う。
Dollar
クラスのオブジェクト化が行えるように、コンストラクタを定義します。
class Dollar:
def __init__(self, amount):
self.amount = amount
(5) 再度、テストを実行し、1つ失敗することを確認する。
Dollar
クラスには、times
メソッドが定義されていないので、テストは失敗します。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testMultiplication FAILED [100%]
================================== FAILURES ===================================
___________________________ MoneyTest.testMultiplication ______________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
File "/Users/ttsubo/source/study_of_test_driven_development/tests/test_money.py", line 7, in testMultiplication
five.times(2)
AttributeError: 'Dollar' object has no attribute 'times'
(6) 再度、小さい変更を行う。
金額に数値を掛け、金額を得る。
ことができるように、times
メソッドが定義します。
class Dollar:
def __init__(self, amount):
self.amount = amount
def times(self, multiplier):
self.amount *= multiplier
(7) 再びテストを実行し、すべて成功することを確認する。-> OK
ようやく、テストが成功するようになりました。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
第2章: 明白な実装 (Degenerate Objects)
第1章では、金額(通貨単位あたりの額)に数値(通貨単位数)を掛け、金額を得る。
という仕組みとして、必要最低限の実装を試みました。さらに、ここでは、金額(通貨単位あたりの額)に数値(通貨単位数)を掛け、金額を得る。
という仕組みを、何度も、呼び出せるように拡張していきます。
(1) まずは、小さいテストを1つ書く。
$5*2=$10
が成立することを確認したのち、$5*3=$15
も成立するテストを作成します。
Dollar
クラスのオブジェクト化を一度行った上で、掛け算の値を色々と変更してテストしていくことを想定します。
from testtools import TestCase
from example.dollar import Dollar
class MoneyTest(TestCase):
def testMultiplication(self):
five = Dollar(5)
product = five.times(2)
self.assertEqual(10, product.amount)
product = five.times(3)
self.assertEqual(15, product.amount)
(2) テストを実行し、1つ失敗することを確認する。
まだ、Dollar
オブジェクトのtimes
メソッドの実行結果を取得するロジックを実装されていないので、テストは失敗します。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testMultiplication FAILED [100%]
================================== FAILURES ===================================
___________________________ MoneyTest.testMultiplication ______________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
File "/Users/ttsubo/source/study_of_test_driven_development/python/tests/test_money.py", line 8, in testMultiplication
self.assertEqual(10, product.amount)
AttributeError: 'NoneType' object has no attribute 'amount'
(3) 小さい変更を行う。
Dollar
オブジェクトのtimes
メソッドの実行結果を取得するロジックを実装します。
-
Dollar
オブジェクトのtimes
メソッド呼び出し時、新たなDollar
オブジェクトを生成する - 新たな
Dollar
オブジェクトのインスタンス変数amount
に、掛け算結果を保存する
class Dollar:
def __init__(self, amount):
self.amount = amount
def times(self, multiplier):
return Dollar(self.amount * multiplier)
(4) 再びテストを実行し、すべて成功することを確認する。-> OK
テストが成功するようになりました。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
第3章: 三角測量 (Equality for All)
第2章では、金額(通貨単位あたりの額)に数値(通貨単位数)を掛け、金額を得る。
という仕組みを、何度も、呼び出せるように拡張しました。ここでは、ある5ドルは、他の5ドルと同じ価値である
ことを確認できる仕組みを追加していきます。ちなみに、pythonの特殊メソッド__eq__
を活用してオブジェクトの同一性を確認しています。
(1) まずは、小さいテストを1つ書く。
ある5ドルは、他の5ドルと等価である
ことを確認するテストを作成します。
from testtools import TestCase
from example.dollar import Dollar
class MoneyTest(TestCase):
def testMultiplication(self):
five = Dollar(5)
product = five.times(2)
self.assertEqual(10, product.amount)
product = five.times(3)
self.assertEqual(15, product.amount)
def testEquality(self):
self.assertTrue(Dollar(5) == Dollar(5))
self.assertFalse(Dollar(5) == Dollar(6))
(2) テストを実行し、1つ失敗することを確認する。
まだ、Dollar
クラスには、ある5ドルは、他の5ドルと等価である
ことが確認できる仕組みが存在しないため、テストは失敗します。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality FAILED [ 50%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
================================== FAILURES ===================================
___________________________ MoneyTest.testMultiplication ______________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
File "/Users/ttsubo/source/study_of_test_driven_development/python/tests/test_money.py", line 13, in testEquality
self.assertTrue(Dollar(5) == Dollar(5))
File "/Users/ttsubo/.pyenv/versions/3.8.0/lib/python3.8/unittest/case.py", line 765, in assertTrue
raise self.failureException(msg)
AssertionError: False is not true
(3) 小さい変更を行う。
特殊メソッド__eq__
を活用して、ある5ドルは、他の5ドルと等価である
ことが確認できるように定義します。
class Dollar:
def __init__(self, amount):
self.amount = amount
def __eq__(self, other):
return self.amount == other.amount
def times(self, multiplier):
return Dollar(self.amount * multiplier)
(4) 再びテストを実行し、すべて成功することを確認する。-> OK
テストが成功するようになりました。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 50%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
第4章: 意図を語るテスト (Privacy)
第3章では、ある5ドルは、他の5ドルと同じ価値である
ことを確認できる仕組みを追加しました。
ここでは、 これまでのテストの記述について、重複している箇所を見通し良くリファクタリングしていきます。
さらに、Dollar
オブジェクトのインスタンス変数amount
をプライベートメンバに変更します。
(1) テスト:MoneyTest:testMultiplication
のリファクタリングを行う。
Dollar
クラスのtimes
メソッドは、自身の金額とmultiplier
引数を掛けた値を、Dollarオブジェクトのインスタンス変数amount
に保持するDollar
オブジェクトが返却されます。
これまで書いていたテスト記述では、Dollar
オブジェクトが返却されることが伝わりにくいため、リファクタリングを行って、可読性を高めていきます。
from testtools import TestCase
from example.dollar import Dollar
class MoneyTest(TestCase):
def testMultiplication(self):
five = Dollar(5)
self.assertEqual(Dollar(10), five.times(2))
self.assertEqual(Dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Dollar(5) == Dollar(5))
self.assertFalse(Dollar(5) == Dollar(6))
(2) テストを実行し、すべて成功することを確認する。
リファクタリング後も、テストは成功しています。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 50%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
(3) 小さい変更を行う。
Dollar
オブジェクトのインスタンス変数amount
をプライベートメンバに変更します。
class Dollar:
def __init__(self, amount):
self.__amount = amount
def __eq__(self, other):
return self.__amount == other.__amount
def times(self, multiplier):
return Dollar(self.__amount * multiplier)
(4) テストを実行し、すべて成功することを確認する。-> OK
テストは成功しています。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 50%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
第5章: 原則をあえて破るとき (Franc-ly Speaking)
第4章までで、多通貨の一つとして、ドル通貨に関わる金額(通貨単位あたりの額)に数値(通貨単位数)を掛け、金額を得る。
の仕組みを実現してきました。ここでは、 別の通貨として、フラン通貨でも同様な仕組みを実現していきます。
(1) まずは、小さいテストを1つ書く。
ドル通貨のテストと同じものを、フラン通貨でも確認できるように、テストを追加します。
from testtools import TestCase
from example.dollar import Dollar
from example.franc import Franc
class MoneyTest(TestCase):
def testMultiplication(self):
five = Dollar(5)
self.assertEqual(Dollar(10), five.times(2))
self.assertEqual(Dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Dollar(5) == Dollar(5))
self.assertFalse(Dollar(5) == Dollar(6))
def testFrancMultiplication(self):
five = Franc(5)
self.assertEqual(Franc(10), five.times(2))
self.assertEqual(Franc(15), five.times(3))
(2) テスト対象のコードを準備しておく
Franc
クラスをロードするだけのコードを配備しておきます。
class Franc:
pass
(3) テストを実行し、1つ失敗することを確認する。
Franc
クラスのオブジェクト化できませんので、テストは失敗します。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication FAILED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
================================== FAILURES ===================================
___________________________ MoneyTest.testMultiplication ______________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
File "/Users/ttsubo/source/study_of_test_driven_development/tests/test_money.py", line 16, in testFrancMultiplication
five = Franc(5)
TypeError: Franc() takes no arguments
(4) 小さい変更を行う。
第1章でやったことを参考に、Franc
クラスを定義していきます。
class Franc:
def __init__(self, amount):
self.__amount = amount
def __eq__(self, other):
return self.__amount == other.__amount
def times(self, multiplier):
return Franc(self.__amount * multiplier)
(4) テストを実行し、すべて成功することを確認する。-> OK
テストは成功しています。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
第6章: テスト不足に気づいたら (Equality for All, Redux)
第5章では、フラン通貨に関わる金額(通貨単位あたりの額)に数値(通貨単位数)を掛け、金額を得る。
の仕組みを実現したわけですが、その方法は、これまでのドル通貨の仕組みをまるまるコピーして対応したため、重複箇所が大量に発生してしまいました。
重複を排除する営みとして、まず、手始めに、第3章で手掛けたある5ドルは、他の5ドルと同じ価値である
とある5フランは、他の5フランと同じ価値である
の部分に着手します。
(1) まずは、小さいテストを1つ書く。
ドル通貨のテストある5ドルは、他の5ドルと等価である
を、フラン通貨でも確認できるように、テストを追加します。
from testtools import TestCase
from example.dollar import Dollar
from example.franc import Franc
class MoneyTest(TestCase):
def testMultiplication(self):
five = Dollar(5)
self.assertEqual(Dollar(10), five.times(2))
self.assertEqual(Dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Dollar(5) == Dollar(5))
self.assertFalse(Dollar(5) == Dollar(6))
self.assertTrue(Franc(5) == Franc(5))
self.assertFalse(Franc(5) == Franc(6))
def testFrancMultiplication(self):
five = Franc(5)
self.assertEqual(Franc(10), five.times(2))
self.assertEqual(Franc(15), five.times(3))
(2) テストを実行し、すべて成功することを確認する。-> OK
追加したテスト部分も、問題なく成功しています。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
(3) リファクタリングを行い、重複を除去する。
ここでは、次のリファクタリングを実施します。
- 親クラス
Money
を新たに定義する -
Dollar
,Franc
クラスで定義していた特殊メソッド__eq__
の重複箇所を排除し、Money
クラスで共通化する
class Money:
def __init__(self, amount):
self.__amount = amount
def __eq__(self, other):
return self.__amount == other.__amount
from example.money import Money
class Dollar(Money):
def __init__(self, amount):
self.__amount = amount
def times(self, multiplier):
return Dollar(self.__amount * multiplier)
from example.money import Money
class Franc(Money):
def __init__(self, amount):
self.__amount = amount
def times(self, multiplier):
return Franc(self.__amount * multiplier)
(4) 再びテストを実行し、すべて成功することを確認する。-> NG
想定外に、テストが全滅してしまいました。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality FAILED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication FAILED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication FAILED [100%]
================================== FAILURES ===================================
______________________________ MoneyTest.testEquality _________________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
...(snip)
File "/Users/ttsubo/source/study_of_test_driven_development/example/money.py", line 6, in __eq__
return self.__amount == other.__amount
AttributeError: 'Dollar' object has no attribute '_Money__amount'
________________________ MoneyTest.testFrancMultiplication ____________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
...(snip)
File "/Users/ttsubo/source/study_of_test_driven_development/example/money.py", line 6, in __eq__
return self.__amount == other.__amount
AttributeError: 'Franc' object has no attribute '_Money__amount'
__________________________ MoneyTest.testMultiplication ______________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
...(snip)
File "/Users/ttsubo/source/study_of_test_driven_development/example/money.py", line 6, in __eq__
return self.__amount == other.__amount
AttributeError: 'Dollar' object has no attribute '_Money__amount'
(5) リファクタリングにより、テストが失敗した原因に対処する。
テストが失敗した原因は、Dollar
, Franc
のオブジェクト化のタイミングで、親クラスMoney
のインスタンス変数amount
にも、値を保存する必要がありましたので、コード修正します。
from example.money import Money
class Dollar(Money):
def __init__(self, amount):
super(Dollar, self).__init__(amount)
self.__amount = amount
def times(self, multiplier):
return Dollar(self.__amount * multiplier)
from example.money import Money
class Franc(Money):
def __init__(self, amount):
super(Franc, self).__init__(amount)
self.__amount = amount
def times(self, multiplier):
return Franc(self.__amount * multiplier)
(6) 再びテストを実行し、すべて成功することを確認する。-> OK
今度は、テストは成功しました。
ちょっとしたリファクタリングのミスにも、すぐに気がつくことができるのは、まさに、テスト駆動開発のおかげですね。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
第7章: 疑念をテストに翻訳する (Apples and Oranges)
これまで、多通貨として、"ドル通貨"と"フラン通貨"に対応してきました。
ここで、疑問に思うのが、「ドル通貨とフラン通貨を比較したらどうなるだろうか?」ということです。
(1) まずは、小さいテストを1つ書く。
ドル通貨とフラン通貨が等しくないことを確認するテストを追加します。
from testtools import TestCase
from example.dollar import Dollar
from example.franc import Franc
class MoneyTest(TestCase):
def testMultiplication(self):
five = Dollar(5)
self.assertEqual(Dollar(10), five.times(2))
self.assertEqual(Dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Dollar(5) == Dollar(5))
self.assertFalse(Dollar(5) == Dollar(6))
self.assertTrue(Franc(5) == Franc(5))
self.assertFalse(Franc(5) == Franc(6))
self.assertFalse(Franc(5) == Dollar(5))
def testFrancMultiplication(self):
five = Franc(5)
self.assertEqual(Franc(10), five.times(2))
self.assertEqual(Franc(15), five.times(3))
(2) テストを実行する。-> NG
テストは失敗します。つまり、ドルとフランが等しいという結果になってしまってます。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality FAILED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
================================== FAILURES ===================================
______________________________ MoneyTest.testEquality _________________________
NOTE: Incompatible Exception Representation, displaying natively:
testtools.testresult.real._StringException: Traceback (most recent call last):
File "/Users/ttsubo/source/study_of_test_driven_development/tests/test_money.py", line 16, in testEquality
self.assertFalse(Franc(5) == Dollar(5))
File "/Users/ttsubo/.pyenv/versions/3.8.0/lib/python3.8/unittest/case.py", line 759, in assertFalse
raise self.failureException(msg)
AssertionError: True is not false
(3) テストが失敗した原因に対処する。
等価性比較を行うには、Dollar
オブジェクトとFranc
オブジェクトを比較する必要がありましたので、Money
クラスの特殊メソッド__eq__
の判定ロジック箇所を、修正します。
class Money:
def __init__(self, amount):
self.__amount = amount
def __eq__(self, other):
return (self.__amount == other.__amount
and self.__class__.__name__ == other.__class__.__name__)
(4) 再びテストを実行し、すべて成功することを確認する。-> OK
今度は、テストは成功しました。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
第8章: 実装を隠す (Makin’ Objects)
第6,7章で、等価性比較に関わるリファクタリングを行いました。
これから、しばらくの間、金額(通貨単位あたりの額)に数値(通貨単位数)を掛け、金額を得る。
という仕組みの重複箇所の排除を試みます。
(1) まずは、小さいテストを書く。
ここでは、Dollar
, Franc
クラスで定義していたtimes
メソッドの重複箇所を排除し、Money
クラスで共通化するための前段として、次のリファクタリングを実施することを想定して、テストを修正します。
-
Money
クラスにクラスメソッドdollar
を定義して、Dollar
オブジェクトを生成するようにする -
Money
クラスにクラスメソッドfranc
を定義して、Franc
オブジェクトを生成するようにする
from testtools import TestCase
from example.money import Money
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertTrue(Money.franc(5) == Money.franc(5))
self.assertFalse(Money.franc(5) == Money.franc(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testFrancMultiplication(self):
five = Money.franc(5)
self.assertEqual(Money.franc(10), five.times(2))
self.assertEqual(Money.franc(15), five.times(3))
(2) リファクタリングを行う。
ここでは、Dollar
, Franc
クラスで定義していたtimes
メソッドの重複箇所を排除し、Money
クラスで共通化するための前段として、次のリファクタリングを実施します。
-
Money
クラスにクラスメソッドdollar
を定義して、Dollar
オブジェクトを生成するようにする -
Money
クラスにクラスメソッドfranc
を定義して、Franc
オブジェクトを生成するようにする
from abc import ABCMeta, abstractmethod
from example.dollar import Dollar
from example.franc import Franc
class Money(metaclass=ABCMeta):
def __init__(self, amount):
self.__amount = amount
def __eq__(self, other):
return (self.__amount == other.__amount
and self.__class__.__name__ == other.__class__.__name__)
@abstractmethod
def times(self, multiplier):
pass
@classmethod
def dollar(cls, amount):
return Dollar(amount)
@classmethod
def franc(cls, amount):
return Franc(amount)
(3) テストを実行する。-> NG
テストが起動できず、エラーになってしまいました。Pythonのモジュールの循環インポートが原因のようですね。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality FAILED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication FAILED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
=================================== ERRORs ====================================
ImportError while importing test module '/Users/ttsubo/source/study_of_test_driven_development/tests/test_money.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
tests/test_money.py:2: in <module>
from example.money import Money
example/money.py:2: in <module>
from example.dollar import Dollar
example/dollar.py:1: in <module>
from example.money import Money
E ImportError: cannot import name 'Money' from partially initialized module 'example.money' (most likely due to a circular import) (/Users/ttsubo/source/study_of_test_driven_development/example/money.py)
(4) テストが失敗した原因に対処する。
Pythonのモジュールの循環インポートが原因なので、Dollar
クラスと、Franc
クラスを、money.pyで定義するように修正します。
from abc import ABCMeta, abstractmethod
class Money(metaclass=ABCMeta):
def __init__(self, amount):
self.__amount = amount
def __eq__(self, other):
return (self.__amount == other.__amount
and self.__class__.__name__ == other.__class__.__name__)
@abstractmethod
def times(self, multiplier):
pass
@classmethod
def dollar(cls, amount):
return Dollar(amount)
@classmethod
def franc(cls, amount):
return Franc(amount)
class Dollar(Money):
def __init__(self, amount):
super(Dollar, self).__init__(amount)
self.__amount = amount
def times(self, multiplier):
return Dollar(self.__amount * multiplier)
class Franc(Money):
def __init__(self, amount):
super(Franc, self).__init__(amount)
self.__amount = amount
def times(self, multiplier):
return Franc(self.__amount * multiplier)
(5) 再びテストを実行し、すべて成功することを確認する。-> OK
今度は、テストは成功しました。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testEquality PASSED [ 33%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
第9章: 歩幅の調整 (Times We’re Livin’ In)
前章に引き続き、金額(通貨単位あたりの額)に数値(通貨単位数)を掛け、金額を得る。
という仕組みの重複箇所の排除を試みます。
(1) まずは、小さいテストを書く。
ここでは、Dollar
, Franc
クラスで定義していたtimes
メソッドの重複箇所を排除し、Money
クラスで共通化するための前段として、Dollar
オブジェクトおよびFranc
オブジェクトを区別することを目的とした**"currency"**という概念を適用すること想定して、テストtestCurrency
を追加します。
-
Dollar
オブジェクトを生成する際に、インスタンス変数__currency
に、"USD"を設定する -
Dollar
オブジェクトのプライベートメンバ__currency
を参照できるように、currency
メソッドを定義する -
Franc
オブジェクトを生成する際に、インスタンス変数__currency
に、"CHF"を設定する -
Franc
オブジェクトのプライベートメンバ__currency
を参照できるように、currency
メソッドを定義する
from testtools import TestCase
from example.money import Money
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertTrue(Money.franc(5) == Money.franc(5))
self.assertFalse(Money.franc(5) == Money.franc(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testFrancMultiplication(self):
five = Money.franc(5)
self.assertEqual(Money.franc(10), five.times(2))
self.assertEqual(Money.franc(15), five.times(3))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
(2) リファクタリングを行う。
ここでは、Dollar
, Franc
クラスで定義していたtimes
メソッドの重複箇所を排除し、Money
クラスで共通化するための前段として、Dollar
オブジェクトおよびFranc
オブジェクトを区別することを目的としたcurrency
という概念を適用します。
-
Dollar
オブジェクトを生成する際に、インスタンス変数__currency
に、"USD"を設定する -
Dollar
オブジェクトのプライベートメンバ__currency
を参照できるように、currency
メソッドを定義する -
Franc
オブジェクトを生成する際に、インスタンス変数__currency
に、"CHF"を設定する -
Franc
オブジェクトのプライベートメンバ__currency
を参照できるように、currency
メソッドを定義する
from abc import ABCMeta, abstractmethod
class Money(metaclass=ABCMeta):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.__class__.__name__ == other.__class__.__name__)
@abstractmethod
def times(self, multiplier):
pass
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Dollar(amount, "USD")
@classmethod
def franc(cls, amount):
return Franc(amount, "CHF")
class Dollar(Money):
def __init__(self, amount, currency):
super().__init__(amount, currency)
self.__amount = amount
def times(self, multiplier):
return Money.dollar(self.__amount * multiplier)
class Franc(Money):
def __init__(self, amount, currency):
super().__init__(amount, currency)
self.__amount = amount
def times(self, multiplier):
return Money.franc(self.__amount * multiplier)
(3) テストを実行し、すべて成功することを確認する。-> OK
期待通りに、テストは成功しました。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 25%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 50%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 75%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
第10章: テストに聞いてみる (Interesting Times)
これまで着手してきた、金額(通貨単位あたりの額)に数値(通貨単位数)を掛け、金額を得る。
という仕組みの重複箇所の排除を試みを完成させます。
(1) まずは、小さいテストを書く。
金額(通貨単位あたりの額)に数値(通貨単位数)を掛け、金額を得る。
という仕組みの重複箇所の排除のリファクタリングが完成したことを想定して、テストtestDifferentClassEquality
を追加します。
from testtools import TestCase
from example.money import Money, Franc
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertTrue(Money.franc(5) == Money.franc(5))
self.assertFalse(Money.franc(5) == Money.franc(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testFrancMultiplication(self):
five = Money.franc(5)
self.assertEqual(Money.franc(10), five.times(2))
self.assertEqual(Money.franc(15), five.times(3))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testDifferentClassEquality(self):
self.assertTrue(Money(10, "CHF") == Franc(10, "CHF"))
(2) リファクタリングを行う。
次のリファクタリングを実施します。
-
Dollar
,Franc
クラスで定義していたtimes
メソッドを削除する -
Money
クラスに、共通化されたtimes
メソッドとして定義する -
Money
オブジェクトの等価性比較のやり方を見直し、特殊メソッド__eq__
の修正する -
Money
オブジェクト生成を想定して、抽象クラスとして扱っていたMoney
クラスも変更する
class Money():
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Dollar(amount, "USD")
@classmethod
def franc(cls, amount):
return Franc(amount, "CHF")
class Dollar(Money):
def __init__(self, amount, currency):
super().__init__(amount, currency)
class Franc(Money):
def __init__(self, amount, currency):
super().__init__(amount, currency)
(3) テストを実行し、すべて成功することを確認する。-> OK
期待通りに、テストは成功しました。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 20%]
tests/test_money.py::MoneyTest::testDifferentClassEquality PASSED [ 40%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 60%]
tests/test_money.py::MoneyTest::testFrancMultiplication PASSED [ 80%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
第11章: 不要になったら消す (The Root of All Evil)
Dollar
, Franc
クラスも、不要にできるので、これまで実施してきたリファクタリングを完結させます。
(1) まずは、小さいテストを書く。
金額(通貨単位あたりの額)に数値(通貨単位数)を掛け、金額を得る。
という仕組みのリファクタリングが完成したことを想定して、テスト自体もリファクタリングします。
from testtools import TestCase
from example.money import Money
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
(2) リファクタリングを行う。
すでに、Money
オブジェクトにて、"ドル通貨"、および"フラン通貨"を区別できるようになっているので、Dollar
, Franc
クラスの定義は、不要になります。
リファクタリングの完結として、Dollar
, Franc
クラスを削除します。
class Money():
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
(3) テストを実行し、すべて成功することを確認する。-> OK
期待通りに、テストは成功しました。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 33%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 66%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [100%]
第12章: 設計とメタファー (Addition, Finally)
これから、通貨の異なる2つの金額を足し、通貨間の為替レートに基づいて換算された金額を得る。
の要件を満足する仕組みを実現していきます。なお、この要件は、次の2つのToDo
に分解することができそうです。
$5 + $5 = $10
$5 + 10 CHF = $10(レートが 2:1 の場合)
まずは、2つの金額を足し、金額を得る。
の仕組みとして、$5 + $5 = $10
が成立するように、コード化を進めています。
(1) まずは、小さいテストを書く。
2つの金額を足し、金額を得る。
の仕組みを確認するため、テストtestSimpleAddition
を追加します。
さらに、今後の設計について、次の二つの概念を念頭に置くものとします。
-
通貨を換算するのは銀行の責務であるべきという考え
に基づくBankオブジェクト
という概念 -
為替レートを適用することによって得られる換算結果
を保存するためのreduced
変数という概念
from testtools import TestCase
from example.money import Money
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
(2) 小さい変更を行う。
この章で扱う実装は、だいぶややこしいので、結構、大きめな変更になってしまいますが、こんな感じです。
-
2つの金額を足し、金額を得る。
の仕組みを実現するために、Money
クラスに、plus
メソッドを追加します。 - 抽象クラス
Expression
を継承して、Money
オブジェクトは生成される -
Bank
クラスのreduce
メソッドについては、ここでは、仮実装とする
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def plus(self, addend):
return Money(self.__amount + addend.__amount, self.__currency)
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
from example.money import Money
class Bank():
def reduce(self, source , toCurrency):
return Money.dollar(10)
from abc import ABCMeta
class Expression(metaclass=ABCMeta):
pass
(3) テストを実行し、すべて成功することを確認する。-> OK
テストは成功しています。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 25%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 50%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 75%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [100%]
第13章: 実装を導くテスト (Make It)
引き続き、2つの金額を足し、金額を得る。
の仕組みとして、$5 + $5 = $10
が成立するように、コード化を進めます。
前回は、Bank
クラスのreduce
メソッドの振る舞いを、仮実装として定義していた箇所に着手します。
なお、ここでの取り組みは、結構なボリュームになりそうなので、以下のステップで進めていきます。
- STEP1:
$5 + $5
の処理を書く - STEP2:
Bank
クラスのreduce
メソッドの仮実装を実体化する - STEP3:
Bank
クラスのreduce
メソッドの仮実装を実体化する(Cont.)
STEP1: $5 + $5
の処理を書く
$5 + $5 = $10
を実現する前段として、$5 + $5
の部分の処理を実体化します。
(1) まずは、小さいテストを書く。
テストtestPlusReturnsSum
を追加します。
from testtools import TestCase
from example.money import Money
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
(2) 小さい変更を行う。
次の変更を行います。
-
Sum
クラスを新たに定義する -
Sum
オブジェクトでは、2つの金額: "augend(被加算数)"と"addend(加数)"の状態を保存する -
Money
クラスのplus
メソッドでは、Sum
オブジェクトを返却するように変更する
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def plus(self, addend):
return Sum(self, addend)
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
class Sum(Expression):
def __init__(self, augend, addend):
self.augend = augend
self.addend = addend
(3) テストを実行し、すべて成功することを確認する。-> OK
テストは成功しています。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 20%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 40%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 60%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 80%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [100%]
STEP2: Bank
クラスのreduce
メソッドの仮実装を実体化する
Bank
クラスのreduce
メソッドを実体化していきます。ここでは、Sum
オブジェクトを対象にします。
(1) まずは、小さいテストを書く。
テストtestReduceSum
を追加します。
from testtools import TestCase
from example.money import Money, Sum
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
def testReduceSum(self):
_sum = Sum(Money.dollar(3), Money.dollar(4))
bank = Bank()
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(7), result)
(2) 小さい変更を行う。
次の変更を行います。
-
Bank
クラスのreduce
メソッドでは、Sum
オブジェクトのreduce
メソッドの処理結果を返却できるようにする -
Sum
クラスのreduce
メソッドにて、2つの金額を足し、金額を得る。
の仕組みを定義する -
Money
クラスに、プライベートメンバ__amount
を外部から参照できるよう、amount
メソッドを定義する
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def plus(self, addend):
return Sum(self, addend)
def amount(self):
return self.__amount
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
class Sum(Expression):
def __init__(self, augend, addend):
self.augend = augend
self.addend = addend
def reduce(self, toCurrency):
amount = self.augend.amount() + self.addend.amount()
return Money(amount, toCurrency)
class Bank():
def reduce(self, source , toCurrency):
return source.reduce(toCurrency)
(3) テストを実行し、すべて成功することを確認する。-> OK
テストは成功しています。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 16%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 33%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 50%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 66%]
tests/test_money.py::MoneyTest::testReduceSum PASSED [ 83%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [100%]
STEP3: Bank
クラスのreduce
メソッドの仮実装を実体化する(Cont.)
Bank
クラスのreduce
メソッドを実体化していきます。ここでは、Money
オブジェクトを対象にします。
(1) まずは、小さいテストを書く。
テストtestReduceMoney
を追加します。
from testtools import TestCase
from example.money import Money, Sum
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
def testReduceSum(self):
_sum = Sum(Money.dollar(3), Money.dollar(4))
bank = Bank()
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(7), result)
def testReduceMoney(self):
bank = Bank()
result = bank.reduce(Money.dollar(1), "USD")
self.assertEqual(Money.dollar(1), result)
(2) 小さい変更を行う。
次の変更を行います。
-
Bank
クラスのreduce
メソッドでは、Money
オブジェクトのreduce
メソッドの処理結果を返却できるようにする -
Money
クラスのreduce
メソッドを定義する(仮実装) - 抽象クラス
Expression
に、抽象メソッドreduce
を定義して、Money
クラスおよび、Sum
クラスでのreduce
メソッド定義を強制させる
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def plus(self, addend):
return Sum(self, addend)
def reduce(self, toCurrency):
return self
def amount(self):
return self.__amount
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
class Sum(Expression):
def __init__(self, augend, addend):
self.augend = augend
self.addend = addend
def reduce(self, toCurrency):
amount = self.augend.amount() + self.addend.amount()
return Money(amount, toCurrency)
from abc import ABCMeta, abstractmethod
class Expression(metaclass=ABCMeta):
@abstractmethod
def reduce(self, toCurrency):
pass
(3) テストを実行し、すべて成功することを確認する。-> OK
テストは成功しています。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 14%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 28%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 42%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 57%]
tests/test_money.py::MoneyTest::testReduceMoney PASSED [ 71%]
tests/test_money.py::MoneyTest::testReduceSum PASSED [ 85%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [100%]
第14章: 学習用テストと回帰テスト (Change)
ここでは、2フランを1ドルに換算する処理を実現していきます。
- STEP1: 2フランを1ドルに換算する処理(為替レートは、仮実装)
- STEP2: 2フランを1ドルに換算する処理(為替レート表の実装)
STEP1: 2フランを1ドルに換算する処理(為替レートは、仮実装)
2フランを1ドルに換算する処理を実現していきます。
ただし、為替レート-> USD:CHF=2:1を前提とします。
(1) まずは、小さいテストを書く。
テストtestReduceMoneyDifferentCurrency
を追加します。
from testtools import TestCase
from example.money import Money, Sum
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
def testReduceSum(self):
_sum = Sum(Money.dollar(3), Money.dollar(4))
bank = Bank()
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(7), result)
def testReduceMoney(self):
bank = Bank()
result = bank.reduce(Money.dollar(1), "USD")
self.assertEqual(Money.dollar(1), result)
def testReduceMoneyDifferentCurrency(self):
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(Money.franc(2), "USD")
self.assertEqual(Money.dollar(1), result)
(2) 小さい変更を行う。
次の変更を行います。
-
Bank
クラスに、add_rate
メソッドする(仮実装) -
Bank
クラスのreduce
メソッドから、Money
オブジェクトのreduce
メソッドを呼び出す際に、自分のBank
オブジェクトを渡すようにする -
Bank
クラスのreduce
メソッドから、Sum
オブジェクトのreduce
メソッドを呼び出す際に、自分のBank
オブジェクトを渡すようにする -
Bank
クラスのrate
メソッドを定義し、暫定的に、為替レートを取得できるようにする(為替レート-> USD:CHF=2:1)
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def plus(self, addend):
return Sum(self, addend)
def reduce(self, bank, toCurrency):
rate = bank.rate(self.__currency, toCurrency)
return Money(self.__amount / rate, toCurrency)
def amount(self):
return self.__amount
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
class Sum(Expression):
def __init__(self, augend, addend):
self.augend = augend
self.addend = addend
def reduce(self, bank, toCurrency):
amount = self.augend.amount() + self.addend.amount()
return Money(amount, toCurrency)
class Bank():
def reduce(self, source , toCurrency):
return source.reduce(self, toCurrency)
def add_rate(self, fromCurrency, toCurrency, rate):
pass
def rate(self, fromCurrency, toCurrency):
return 2 if (fromCurrency == "CHF" and toCurrency == "USD") else 1
from abc import ABCMeta, abstractmethod
class Expression(metaclass=ABCMeta):
@abstractmethod
def reduce(self, bank, toCurrency):
pass
(3) テストを実行し、すべて成功することを確認する。-> OK
テストは成功しています。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 12%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 25%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 37%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 50%]
tests/test_money.py::MoneyTest::testReduceMoney PASSED [ 62%]
tests/test_money.py::MoneyTest::testReduceMoneyDifferentCurrency PASSED [ 75%]
tests/test_money.py::MoneyTest::testReduceSum PASSED [ 87%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [100%]
STEP2: 2フランを1ドルに換算する処理(為替レート表の実装)
為替レート表に基づき、2フランを1ドルに換算する処理を実現していきます。
(1) まずは、小さいテストを書く。
テストtestIdentityRate
を追加します。
from testtools import TestCase
from example.money import Money, Sum
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
def testReduceSum(self):
_sum = Sum(Money.dollar(3), Money.dollar(4))
bank = Bank()
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(7), result)
def testReduceMoney(self):
bank = Bank()
result = bank.reduce(Money.dollar(1), "USD")
self.assertEqual(Money.dollar(1), result)
def testReduceMoneyDifferentCurrency(self):
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(Money.franc(2), "USD")
self.assertEqual(Money.dollar(1), result)
def testIdentityRate(self):
self.assertEqual(1, Bank().rate("USD", "USD"))
(2) 小さい変更を行う。
次の変更を行います。
-
Bank
クラスにて、為替レート表を保持できるようにする -
Bank
オブジェクトのadd_rate
メソッドにより、為替レート表に各種レートを追加できる -
Bank
オブジェクトのrate
メソッドにより、必要なレートを参照できる
class Bank():
def __init__(self):
self._rates = {}
def reduce(self, source , toCurrency):
return source.reduce(self, toCurrency)
def add_rate(self, fromCurrency, toCurrency, rate):
target_rate = "{0}:{1}".format(fromCurrency, toCurrency)
self._rates[target_rate] = rate
def rate(self, fromCurrency, toCurrency):
target_rate = "{0}:{1}".format(fromCurrency, toCurrency)
if fromCurrency == toCurrency:
return 1
return self._rates.get(target_rate)
(3) テストを実行し、すべて成功することを確認する。-> OK
テストは成功しています。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 11%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 22%]
tests/test_money.py::MoneyTest::testIdentityRate PASSED [ 33%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 44%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 55%]
tests/test_money.py::MoneyTest::testReduceMoney PASSED [ 66%]
tests/test_money.py::MoneyTest::testReduceMoneyDifferentCurrency PASSED [ 77%]
tests/test_money.py::MoneyTest::testReduceSum PASSED [ 88%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [100%]
第15章: テスト任せとコンパイラ任せ (Mixed Currencies)
これまで、通貨の異なる2つの金額を足し、通貨間の為替レートに基づいて換算された金額を得る。
の要件を満足する仕組みの実現化を目指してきました。ここでは、$5 + 10 CHF = $10(レートが 2:1 の場合)
のToDoに着手していきます。
(1) まずは、小さいテストを書く。
テストtestMixedAddition
を追加します。
from testtools import TestCase
from example.money import Money, Sum
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
def testReduceSum(self):
_sum = Sum(Money.dollar(3), Money.dollar(4))
bank = Bank()
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(7), result)
def testReduceMoney(self):
bank = Bank()
result = bank.reduce(Money.dollar(1), "USD")
self.assertEqual(Money.dollar(1), result)
def testReduceMoneyDifferentCurrency(self):
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(Money.franc(2), "USD")
self.assertEqual(Money.dollar(1), result)
def testIdentityRate(self):
self.assertEqual(1, Bank().rate("USD", "USD"))
def testMixedAddition(self):
fiveBucks = Money.dollar(5)
tenFrancs = Money.franc(10)
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(fiveBucks.plus(tenFrancs), "USD")
self.assertEqual(Money.dollar(10), result)
(2) 小さい変更を行う。
次の変更を行います。
-
Sum
オブジェクトのreduce
メソッドでのamount
導出方法を修正する -
Sum
クラスに、plus
メソッドを定義する - 抽象クラス
Expression
に、抽象メソッドplus
を定義して、Money
クラスおよび、Sum
クラスでのplus
メソッド定義を強制させる
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def plus(self, addend):
return Sum(self, addend)
def reduce(self, bank, toCurrency):
rate = bank.rate(self.__currency, toCurrency)
return Money(self.__amount / rate, toCurrency)
def amount(self):
return self.__amount
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
class Sum(Expression):
def __init__(self, augend, addend):
self.augend = augend
self.addend = addend
def reduce(self, bank, toCurrency):
amount = self.augend.reduce(bank, toCurrency).amount() + \
self.addend.reduce(bank, toCurrency).amount()
return Money(amount, toCurrency)
def plus(self, addend):
pass
from abc import ABCMeta, abstractmethod
class Expression(metaclass=ABCMeta):
@abstractmethod
def plus(self, addend):
pass
@abstractmethod
def reduce(self, bank, toCurrency):
pass
(3) テストを実行し、すべて成功することを確認する。-> OK
テストは成功しています。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 10%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 20%]
tests/test_money.py::MoneyTest::testIdentityRate PASSED [ 30%]
tests/test_money.py::MoneyTest::testMixedAddition PASSED [ 40%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 50%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 60%]
tests/test_money.py::MoneyTest::testReduceMoney PASSED [ 70%]
tests/test_money.py::MoneyTest::testReduceMoneyDifferentCurrency PASSED [ 80%]
tests/test_money.py::MoneyTest::testReduceSum PASSED [ 90%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [100%]
第16章: 将来の読み手を考えたテスト (Abstraction, Finally)
いよいよ、通貨の異なる2つの金額を足し、通貨間の為替レートに基づいて換算された金額を得る。
の要件を満足する仕組みの実現化を完成させます。
- STEP1:
Sum
クラスのplus
メソッドを完成させる - STEP2:
Sum
クラスのtimes
メソッドを完成させる
STEP1: Sum
クラスのplus
メソッドを完成させる
Sum
クラスのplus
メソッドを完成させます。
(1) まずは、小さいテストを書く。
テストtestSumPlusMoney
を追加します。
from testtools import TestCase
from example.money import Money, Sum
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
def testReduceSum(self):
_sum = Sum(Money.dollar(3), Money.dollar(4))
bank = Bank()
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(7), result)
def testReduceMoney(self):
bank = Bank()
result = bank.reduce(Money.dollar(1), "USD")
self.assertEqual(Money.dollar(1), result)
def testReduceMoneyDifferentCurrency(self):
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(Money.franc(2), "USD")
self.assertEqual(Money.dollar(1), result)
def testIdentityRate(self):
self.assertEqual(1, Bank().rate("USD", "USD"))
def testMixedAddition(self):
fiveBucks = Money.dollar(5)
tenFrancs = Money.franc(10)
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(fiveBucks.plus(tenFrancs), "USD")
self.assertEqual(Money.dollar(10), result)
def testSumPlusMoney(self):
fiveBucks = Money.dollar(5)
tenFrancs = Money.franc(10)
bank = Bank()
bank.add_rate("CHF", "USD", 2)
_sum = Sum(fiveBucks, tenFrancs).plus(fiveBucks)
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(15), result)
(2) 小さい変更を行う。
次の変更を行います。
-
Sum
オブジェクトの、plus
メソッドが呼ばれたとき、Sum
オブジェクトを返却する - 抽象クラス
Expression
に、抽象メソッドplus
を定義して、Money
クラスおよび、Sum
クラスでのplus
メソッド定義を強制させる
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.amount = amount
self._currency = currency
def __eq__(self, other):
return (self.amount == other.amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.amount * multiplier, self._currency)
def plus(self, addend):
return Sum(self, addend)
def reduce(self, bank, toCurrency):
rate = bank.rate(self.currency(), toCurrency)
return Money(self.amount / rate, toCurrency)
def currency(self):
return self._currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
class Sum(Expression):
def __init__(self, augend, addend):
self.augend = augend
self.addend = addend
def reduce(self, bank, toCurrency):
amount = self.augend.reduce(bank, toCurrency).amount + \
self.addend.reduce(bank, toCurrency).amount
return Money(amount, toCurrency)
def plus(self, addend):
return Sum(self, addend)
class Bank():
def __init__(self):
self._rates = {}
def reduce(self, source , toCurrency):
return source.reduce(self, toCurrency)
def add_rate(self, fromCurrency, toCurrency, rate):
self._rates[(fromCurrency, toCurrency)] = rate
def rate(self, fromCurrency, toCurrency):
if fromCurrency == toCurrency:
return 1
return self._rates.get((fromCurrency, toCurrency))
(3) テストを実行し、すべて成功することを確認する。-> OK
テストは成功しています。
$ pytest -v
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 9%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 18%]
tests/test_money.py::MoneyTest::testIdentityRate PASSED [ 27%]
tests/test_money.py::MoneyTest::testMixedAddition PASSED [ 36%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 45%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 54%]
tests/test_money.py::MoneyTest::testReduceMoney PASSED [ 63%]
tests/test_money.py::MoneyTest::testReduceMoneyDifferentCurrency PASSED [ 72%]
tests/test_money.py::MoneyTest::testReduceSum PASSED [ 81%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [ 90%]
tests/test_money.py::MoneyTest::testSumPlusMoney PASSED [100%]
STEP2: Sum
クラスのtimes
メソッドを完成させる
Sum
クラスのtimes
メソッドを完成させます。
(1) まずは、小さいテストを書く。
テストtestSumTimes
を追加します。
from testtools import TestCase
from example.money import Money, Sum
from example.bank import Bank
class MoneyTest(TestCase):
def testMultiplication(self):
five = Money.dollar(5)
self.assertEqual(Money.dollar(10), five.times(2))
self.assertEqual(Money.dollar(15), five.times(3))
def testEquality(self):
self.assertTrue(Money.dollar(5) == Money.dollar(5))
self.assertFalse(Money.dollar(5) == Money.dollar(6))
self.assertFalse(Money.franc(5) == Money.dollar(5))
def testCurrency(self):
self.assertEqual("USD", Money.dollar(1).currency())
self.assertEqual("CHF", Money.franc(1).currency())
def testSimpleAddition(self):
five = Money.dollar(5)
_sum = five.plus(five)
bank = Bank()
reduced = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(10), reduced)
def testPlusReturnsSum(self):
five = Money.dollar(5)
_sum = five.plus(five)
self.assertEqual(five, _sum.augend)
self.assertEqual(five, _sum.addend)
def testReduceSum(self):
_sum = Sum(Money.dollar(3), Money.dollar(4))
bank = Bank()
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(7), result)
def testReduceMoney(self):
bank = Bank()
result = bank.reduce(Money.dollar(1), "USD")
self.assertEqual(Money.dollar(1), result)
def testReduceMoneyDifferentCurrency(self):
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(Money.franc(2), "USD")
self.assertEqual(Money.dollar(1), result)
def testIdentityRate(self):
self.assertEqual(1, Bank().rate("USD", "USD"))
def testMixedAddition(self):
fiveBucks = Money.dollar(5)
tenFrancs = Money.franc(10)
bank = Bank()
bank.add_rate("CHF", "USD", 2)
result = bank.reduce(fiveBucks.plus(tenFrancs), "USD")
self.assertEqual(Money.dollar(10), result)
def testSumPlusMoney(self):
fiveBucks = Money.dollar(5)
tenFrancs = Money.franc(10)
bank = Bank()
bank.add_rate("CHF", "USD", 2)
_sum = Sum(fiveBucks, tenFrancs).plus(fiveBucks)
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(15), result)
def testSumTimes(self):
fiveBucks = Money.dollar(5)
tenFrancs = Money.franc(10)
bank = Bank()
bank.add_rate("CHF", "USD", 2)
_sum = Sum(fiveBucks, tenFrancs).times(2)
result = bank.reduce(_sum, "USD")
self.assertEqual(Money.dollar(20), result)
(2) 小さい変更を行う。
次の変更を行います。
-
Sum
オブジェクトの、times
メソッドが呼ばれたとき、Sum
オブジェクトを返却する - 抽象クラス
Expression
に、抽象メソッドtimes
を定義して、Money
クラスおよび、Sum
クラスでのtimes
メソッド定義を強制させる
from example.expression import Expression
class Money(Expression):
def __init__(self, amount, currency):
self.__amount = amount
self.__currency = currency
def __eq__(self, other):
return (self.__amount == other.__amount
and self.currency() == other.currency())
def times(self, multiplier):
return Money(self.__amount * multiplier, self.__currency)
def plus(self, addend):
return Sum(self, addend)
def reduce(self, bank, toCurrency):
rate = bank.rate(self.__currency, toCurrency)
return Money(self.__amount / rate, toCurrency)
def amount(self):
return self.__amount
def currency(self):
return self.__currency
@classmethod
def dollar(cls, amount):
return Money(amount, "USD")
@classmethod
def franc(cls, amount):
return Money(amount, "CHF")
class Sum(Expression):
def __init__(self, augend, addend):
self.augend = augend
self.addend = addend
def reduce(self, bank, toCurrency):
amount = self.augend.reduce(bank, toCurrency).amount() + \
self.addend.reduce(bank, toCurrency).amount()
return Money(amount, toCurrency)
def plus(self, addend):
return Sum(self, addend)
def times(self, multiplier):
return Sum(self.augend.times(multiplier), self.addend.times(multiplier))
from abc import ABCMeta, abstractmethod
class Expression(metaclass=ABCMeta):
@abstractmethod
def plus(self, addend):
pass
@abstractmethod
def reduce(self, bank, toCurrency):
pass
@abstractmethod
def times(self, multiplier):
pass
(3) テストを実行し、すべて成功することを確認する。-> OK
テストは成功していますし、カバレッジも概ね良好です。
$ pytest -v --cov=example
...(snip)
tests/test_money.py::MoneyTest::testCurrency PASSED [ 8%]
tests/test_money.py::MoneyTest::testEquality PASSED [ 16%]
tests/test_money.py::MoneyTest::testIdentityRate PASSED [ 25%]
tests/test_money.py::MoneyTest::testMixedAddition PASSED [ 33%]
tests/test_money.py::MoneyTest::testMultiplication PASSED [ 41%]
tests/test_money.py::MoneyTest::testPlusReturnsSum PASSED [ 50%]
tests/test_money.py::MoneyTest::testReduceMoney PASSED [ 58%]
tests/test_money.py::MoneyTest::testReduceMoneyDifferentCurrency PASSED [ 66%]
tests/test_money.py::MoneyTest::testReduceSum PASSED [ 75%]
tests/test_money.py::MoneyTest::testSimpleAddition PASSED [ 83%]
tests/test_money.py::MoneyTest::testSumPlusMoney PASSED [ 91%]
tests/test_money.py::MoneyTest::testSumTimes PASSED [100%]
...(snip)
---------- coverage: platform darwin, python 3.8.0-final-0 -----------
Name Stmts Miss Cover
-------------------------------------------
example/__init__.py 0 0 100%
example/bank.py 13 0 100%
example/expression.py 11 3 73%
example/money.py 35 0 100%
-------------------------------------------
TOTAL 59 3 95%
...(snip)
以上で、テスト駆動開発を活用したPython版「多国通貨」開発を体験してみました。
■ 終わりに ...
書籍「テスト駆動開発」の第Ⅱ部「xUnit」でも、Pythonで、テスト駆動開発が体験できます。
実際に、体験してみて気が付いたのですが、テスト駆動開発は、私がイメージしていたテストとだいぶ乖離していて、完全に誤解していました。
書籍の第32章「TDDを身につける」からの引用ですが、まさに、これです!!
皮肉なことに、TDD はテスト技法ではない(Cunningham の公案)。 TDD は分析技法であり、設計技法であり、実際には開発のすべてのアク ティビティを構造化する技法なのだ。
あと、付録C「訳者解説:テスト駆動開発の現在」は、TDD/BDDに対する理解を助けるものであり、ものすごく勉強になりました。