pyhaya’s diary

機械学習系の記事をメインで書きます

「テスト駆動開発」をPythonで書き直してみた 6

書籍「テスト駆動開発」をPythonで書き直してみたシリーズの第6弾です。すでに書籍のコードとは大きく乖離し始めていますが一応参考書籍は明示しておきます。過去の記事はこちらです。
pyhaya.hatenablog.com


テスト駆動開発

テスト駆動開発

通貨同士の足し算を実装する

最初に通貨の掛け算はtimesメソッドで実装していました。今回は、足し算を実装します。足し算の場合には掛け算とは異なり書けるほうもかけられるほうもMoneyオブジェクトであることに注意しなくてはいけません。

まずはテストから書きます。

テストを書く

tests/test_money.py

import sys
sys.path.append('../src')

import unittest
from money import Money

class MoneyTest(unittest.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 testFrancMultiplication(self):
        five = Money.franc(5)
        self.assertEqual(Money.franc(10), five.times(2))
        self.assertEqual(Money.franc(15), five.times(3))

    def testEquality(self):
        self.assertNotEqual(Money.franc(5), Money.dollar(5))

    def testSimpleAddition(self):    # <- 追加
        sum_ = Money.dollar(5).plus(Money.dollar(5))
        self.assertEqual(sum_, Money.dollar(10))

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

テストが通るようにコードを書きなおす

テストを通します。
src/money.py

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

    def __eq__(self, other):
        return self.__dict__ == other.__dict__
    
    @staticmethod
    def dollar(amount):
        return Money(amount, 'USD')

    @staticmethod
    def franc(amount):
        return Money(amount, 'CHF')

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

    def plus(self, addend):
        amount = self.amount + addend.amount
        return Money(amount, self.currency)

もう少し頑張る

これで通貨の掛け算、そして通貨同士の足し算を実装できたわけですが、何か違和感がありました。Pythonでは特殊メソッドを使って基本的な演算が簡単に実装できるので、これを使ったほうが自然なコードになるはずです。

実現したいことをテストで表現します。

tests/test_money.py

import sys
sys.path.append('../src')

import unittest
from money import Money

class MoneyTest(unittest.TestCase):
    def testMultiplication(self):
        five = Money.dollar(5)
        self.assertEqual(Money.dollar(10), five * 2)    # <- 変更
        self.assertEqual(Money.dollar(15), five * 3)    # <- 変更

    def testFrancMultiplication(self):
        five = Money.franc(5)
        self.assertEqual(Money.franc(10), five * 2)    # <- 変更
        self.assertEqual(Money.franc(15), five * 3)    # <- 変更

    def testEquality(self):
        self.assertNotEqual(Money.franc(5), Money.dollar(5))

    def testSimpleAddition(self):
        sum_ = Money.dollar(5) + Money.dollar(5)    # <- 変更
        self.assertEqual(sum_, Money.dollar(10))

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

やはりこのほうが自然な気がします。では、これに合わせてソースコードを変更します。

src/money.py

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

    def __eq__(self, other):
        return self.__dict__ == other.__dict__

    def __add__(self, other):
        return Money(self.amount + other.amount, self.currency)

    def __mul__(self, multiplier):
        return Money(self.amount * multiplier, self.currency)
    
    @staticmethod
    def dollar(amount):
        return Money(amount, 'USD')

    @staticmethod
    def franc(amount):
        return Money(amount, 'CHF')

これで通ります。

テストを見直す

この段階でもう一度テストコードを見直してみます。すると、testFrancMultiplicationはもう必要ない気がしてきます。これは前回までで、FrancクラスとDollarクラスが分かれていたからこそ意味があったものでMoneyクラスに統合された状態ではtestMultiplicationでもう十分信頼性を確かめられています。

import sys
sys.path.append('../src')

import unittest
from money import Money

class MoneyTest(unittest.TestCase):
    def testMultiplication(self):
        five = Money.dollar(5)
        self.assertEqual(Money.dollar(10), five * 2)
        self.assertEqual(Money.dollar(15), five * 3)

    def testEquality(self):
        self.assertNotEqual(Money.franc(5), Money.dollar(5))

    def testSimpleAddition(self):
        sum_ = Money.dollar(5) + Money.dollar(5)
        self.assertEqual(sum_, Money.dollar(10))

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

コードはGitHub上にありますので、ご自由にお使いください。
github.com

次回記事はこちら
pyhaya.hatenablog.com