# 10章 リファクタリング¶

## 飛び込む¶

In [1]:
import re

roman_numeral_map = (
('M',  1000),
('CM', 900),
('D',  500),
('CD', 400),
('C',  100),
('XC', 90),
('L',  50),
('XL', 40),
('X',  10),
('IX', 9),
('V',  5),
('IV', 4),
('I',  1)
)

roman_numeral_pattern = re.compile('''
^                   # beginning of string
M{0,3}              # thousands - 0 to 3 Ms
(CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),
#            or 500-800 (D, followed by 0 to 3 Cs)
(XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs),
#        or 50-80 (L, followed by 0 to 3 Xs)
(IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is),
#        or 5-8 (V, followed by 0 to 3 Is)
$# end of string ''', re.VERBOSE) def from_roman(s): '''convert Roman numeral to integer''' if not roman_numeral_pattern.search(s): raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s)) result = 0 index = 0 for numeral, integer in roman_numeral_map: while s[index : index + len(numeral)] == numeral: result += integer index += len(numeral) return result  これはバグ。 空白文字列に対しては、有効なローマ数字となっていない他の文字列の場合と同じように、 InvalidRomanNumeralError例外が送出されなくてはならない。 In [2]: from_roman('')  Out[2]: 0 バグを再現できたら、それを修正するのに先立って、パスしないようなテストケースを書かなければならない。こうしてバグをはっきりと示す。 In [3]: import unittest class InvalidRomanNumeralError(ValueError): pass class FromRomanBadInput(unittest.TestCase): def test_too_many_repeated_numerals(self): '''from_roman should fail with too many repeated numerals''' for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'): self.assertRaises(InvalidRomanNumeralError, from_roman, s) def test_repeated_pairs(self): '''from_roman should fail with repeated pairs of numerals''' for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'): self.assertRaises(InvalidRomanNumeralError, from_roman, s) def test_malformed_antecedents(self): '''from_roman should fail with malformed antecedents''' for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'): self.assertRaises(InvalidRomanNumeralError, from_roman, s) # 新しく追加 def testBlank(self): '''from_roman should fail with blank string''' self.assertRaises(InvalidRomanNumeralError, from_roman, '')  すなわち、from_roman()を空白文字列とともに呼び出し、 InvalidRomanNumeralError例外が送出されるかどうかを確認する。 難しいのはバグを見つけ出す段階であって、一度分かってしまえば、そのバグをテストするのはごく簡単。 In [4]: %tb test = unittest.TestLoader().loadTestsFromTestCase(FromRomanBadInput) unittest.TextTestRunner().run(test)  No traceback available to show. F... ====================================================================== FAIL: testBlank (__main__.FromRomanBadInput) from_roman should fail with blank string ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-3-f620bb1328bb>", line 31, in testBlank self.assertRaises(InvalidRomanNumeralError, from_roman, '') AssertionError: InvalidRomanNumeralError not raised by from_roman ---------------------------------------------------------------------- Ran 4 tests in 0.011s FAILED (failures=1)  Out[4]: <unittest.runner.TextTestResult run=4 errors=0 failures=1> これでようやくこのバグを修正できる。 たった二行のコードを追加するだけで足りる。 一行目で明示的に空白文字列をチェックし、二行目にはraise文 をあてる。 In [5]: class OutOfRangeError(ValueError): pass class NotIntegerError(ValueError): pass def to_roman(n): '''convert integer to Roman numeral''' if not (0 < n < 4000): raise OutOfRangeError('number out of range (must be 1..3999)') if not isinstance(n, int): raise NotIntegerError('non-integers can not be converted') result = '' for numeral, integer in roman_numeral_map: while n >= integer: result += numeral n -= integer return result  In [6]: roman_numeral_pattern = re.compile(''' ^ # beginning of string M{0,3} # thousands - 0 to 3 Ms (CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs), # or 500-800 (D, followed by 0 to 3 Cs) (XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs), # or 50-80 (L, followed by 0 to 3 Xs) (IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is), # or 5-8 (V, followed by 0 to 3 Is)$                   # end of string
''', re.VERBOSE)

def from_roman(s):
'''convert Roman numeral to integer'''
if not s:
raise InvalidRomanNumeralError('Input can not be blank')
if not re.search(roman_numeral_pattern, s):
raise InvalidRomanNumeralError('Invalid Roman numeral: {}'.format(s))

result = 0
index = 0
for numeral, integer in roman_numeral_map:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result

'Invalid Roman numeral: {}'.format(s)

Python 3.1からは、フォーマット指定子の中でインデクスを使用する場合に数字を省略できるようになっている。 つまり、format()メソッドの最初の引数を参照する場合に、フォーマット指定子の{0}を使わなくとも、 単純に {} とすればPython が自動で適当なインデクスを埋めてくれる。

これは引数がいくつあっても機能するもので、 最初の{}は{0} に、次の{}は{1} というように解釈されていく。

In [7]:
%tb
test = unittest.TestLoader().loadTestsFromTestCase(FromRomanBadInput)
unittest.TextTestRunner().run(test)

No traceback available to show.
....
----------------------------------------------------------------------
Ran 4 tests in 0.009s

OK

Out[7]:
<unittest.runner.TextTestResult run=4 errors=0 failures=0>

このようにコーディングしても、別にバグが修正しやすくなるわけではない。 （このバグのように）簡単なバグには簡単なテストケースで足りるが、 複雑なバグには複雑なテストケースが必要となる。

もしかしたら、テスト中心の開発環境だとバグを修正するのに長い時間がかかるように見えるかもしれない。 まずもって、（テストケースを書いて）何がバグなのかをコードの中で明確に示してからバグを修正しなくてはならないし、 しかもそこでテストケースをパスしなかったら、修正が間違っていたのか、 それともテストケース自体にバグがあるのかを調べなくてはならない。

しかし、長い目で見れば、テストコードとそのテストを受けるコードの間を行ったり来たりすることは十分割に合うことだろう。 こうすれば、バグが最初の一回で正しく修正されやすくなるし、 新しいテストケースと一緒に他のすべてのテストケースを再実行することも簡単にできるので、 新しいコードを加える際に古いコードを壊してしまうということが非常に少なくなる。 今日のユニットテストは、明日の回帰テストとなる。

## 要件の変更に対処する¶

どれだけ顧客から厳密な要件を引き出しても、要件というのは変わってしまう。 そもそも、ほとんどの顧客は実際に見てみるまで、自分が何を求めているのかを理解しないもの。

たとえ理解していたとしても、顧客はどういうものがあれば十分なのかをはっきりと伝えるのが上手ではない。 仮に明確に伝えてきたとしても、どのみち次のリリースの時には別の機能も欲しがるようになっている。 だから、要件の変更に合わせてテストケースをアップデートできるようにしておく。

すでにタプルに入っている既知の値については何も変更を加えないが（これらはまだテストする意味のある値だ）、 4000台の数をいくつか加える必要がある。 ここでは

• 4000（最も短い数）、
• 4500（二番目に短い数）、
• 4888（最も長い数）、
• 4999（最大の数）

を加えてある。

In [8]:
class KnownValues(unittest.TestCase):
known_values = ((1, 'I'),
(2, 'II'),
(3, 'III'),
(4, 'IV'),
(5, 'V'),
(6, 'VI'),
(7, 'VII'),
(8, 'VIII'),
(9, 'IX'),
(10, 'X'),
(50, 'L'),
(100, 'C'),
(500, 'D'),
(1000, 'M'),
(31, 'XXXI'),
(148, 'CXLVIII'),
(294, 'CCXCIV'),
(312, 'CCCXII'),
(421, 'CDXXI'),
(528, 'DXXVIII'),
(621, 'DCXXI'),
(782, 'DCCLXXXII'),
(870, 'DCCCLXX'),
(941, 'CMXLI'),
(1043, 'MXLIII'),
(1110, 'MCX'),
(1226, 'MCCXXVI'),
(1301, 'MCCCI'),
(1485, 'MCDLXXXV'),
(1509, 'MDIX'),
(1607, 'MDCVII'),
(1754, 'MDCCLIV'),
(1832, 'MDCCCXXXII'),
(1993, 'MCMXCIII'),
(2074, 'MMLXXIV'),
(2152, 'MMCLII'),
(2212, 'MMCCXII'),
(2343, 'MMCCCXLIII'),
(2499, 'MMCDXCIX'),
(2574, 'MMDLXXIV'),
(2646, 'MMDCXLVI'),
(2723, 'MMDCCXXIII'),
(2892, 'MMDCCCXCII'),
(2975, 'MMCMLXXV'),
(3051, 'MMMLI'),
(3185, 'MMMCLXXXV'),
(3250, 'MMMCCL'),
(3313, 'MMMCCCXIII'),
(3408, 'MMMCDVIII'),
(3501, 'MMMDI'),
(3610, 'MMMDCX'),
(3743, 'MMMDCCXLIII'),
(3844, 'MMMDCCCXLIV'),
(3888, 'MMMDCCCLXXXVIII'),
(3940, 'MMMCMXL'),
(3999, 'MMMCMXCIX'),
(4000, 'MMMM'),
(4500, 'MMMMD'),
(4888, 'MMMMDCCCLXXXVIII'),
(4999, 'MMMMCMXCIX')
)

def test_to_roman_known_values(self):
'''to_roman should give known result with known input'''

for integer, numeral in self.known_values:
result = to_roman(integer)
self.assertEqual(numeral, result)

def test_from_roman_known_values(self):
'''from_roman should give known result with known input'''

for integer, numeral in self.known_values:
result = from_roman(numeral)
self.assertEqual(integer, result)



「大きな入力値」の定義が変わっている。 このテストは to_roman() を4000と共に呼び出してエラーが出るかを確認するものだったが、 4000-4999は有効な入力値となったので、引数の値を5000まで引き上げなくてはならない。

In [9]:
class ToRomanBadInput(unittest.TestCase):

def test_too_large(self):
'''to_roman should fail with large input'''
self.assertRaises(OutOfRangeError, to_roman, 5000)



「繰り返され過ぎている数字」の定義も変わっている。 このテストは from_roman() を'MMMM'とともに呼び出してエラーが出るかを確認するものだったが、 MMMMは有効なローマ数字となったので、引数の値を'MMMMM'まで引き上げなくてはならない。,

In [10]:
class FromRomanBadInput(unittest.TestCase):

def test_too_many_repeated_numerals(self):
'''from_roman should fail with too many repeated numerals'''
for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
self.assertRaises(InvalidRomanNumeralError, from_roman, s)

def test_repeated_pairs(self):
'''from_roman should fail with repeated pairs of numerals'''
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
self.assertRaises(InvalidRomanNumeralError, from_roman, s)

def test_malformed_antecedents(self):
'''from_roman should fail with malformed antecedents'''
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', 'MCMC', 'XCX', 'IVI',
'LM', 'LD', 'LC'):
self.assertRaises(InvalidRomanNumeralError, from_roman, s)

# 新しく追加
def testBlank(self):
'''from_roman should fail with blank string'''
self.assertRaises(InvalidRomanNumeralError, from_roman, '')



この動作確認テストでは 1 から 3999 までのすべての数をループしていたが、 値の範囲が拡張されたのに合わせて、このforループも4999 までの数をテストするように修正しなければならない。

In [11]:
class RoundtripCheck(unittest.TestCase):

def test_roundtrip(self):
'''from_roman(to_roman(n))==n for all n'''

for integer in range(1, 5000):
numeral = to_roman(integer)
result = from_roman(numeral)
self.assertEqual(integer, result)



from_roman() の既知の値のテストは'MMMM'に突き当たったところで失敗する。 from_roman() がまだこのローマ数字を無効なものとしているからだ。

to_roman() の既知の値のテストは4000に突き当たったところで失敗する。 to_roman() がまだこれを範囲外の数としているため。

In [12]:
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(FromRomanBadInput)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(RoundtripCheck)
unittest.TextTestRunner().run(test)

No traceback available to show.
EE
======================================================================
ERROR: test_from_roman_known_values (__main__.KnownValues)
from_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-8-051340391a19>", line 76, in test_from_roman_known_values
result = from_roman(numeral)
File "<ipython-input-6-0ddb24c33a8a>", line 18, in from_roman
raise InvalidRomanNumeralError('Invalid Roman numeral: {}'.format(s))
InvalidRomanNumeralError: Invalid Roman numeral: MMMM

======================================================================
ERROR: test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-8-051340391a19>", line 68, in test_to_roman_known_values
result = to_roman(integer)
File "<ipython-input-5-8a6a496128ad>", line 11, in to_roman
raise OutOfRangeError('number out of range (must be 1..3999)')
OutOfRangeError: number out of range (must be 1..3999)

----------------------------------------------------------------------
Ran 2 tests in 0.008s

FAILED (errors=2)
....
----------------------------------------------------------------------
Ran 4 tests in 0.009s

OK
E
======================================================================
ERROR: test_roundtrip (__main__.RoundtripCheck)
from_roman(to_roman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-11-cce19637b0ef>", line 7, in test_roundtrip
numeral = to_roman(integer)
File "<ipython-input-5-8a6a496128ad>", line 11, in to_roman
raise OutOfRangeError('number out of range (must be 1..3999)')
OutOfRangeError: number out of range (must be 1..3999)

----------------------------------------------------------------------
Ran 1 test in 0.128s

FAILED (errors=1)

Out[12]:
<unittest.runner.TextTestResult run=1 errors=1 failures=0>

これで新しい要件によって失敗するテストケースができ上がったので、これらのテストをパスするようにコードを修正する作業に移ることができる

（ユニットテストをコーディングし始めた最初のうちは、テスト対象のコードが決してユニットテストの「先」にこないことに違和感を覚えるかもしれない。コードが追いついていないのに、 まだやるべき作業があって、 そしてコードがテストケースに追いついたら、 即座にコーディングをやめる。）。

In [13]:
roman_numeral_pattern = re.compile('''
^                   # beginning of string
M{0,4}              # thousands - 0 to 4 Ms  MMMMまで許すよう変更
(CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),
#            or 500-800 (D, followed by 0 to 3 Cs)
(XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs),
#        or 50-80 (L, followed by 0 to 3 Xs)
(IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is),
#        or 5-8 (V, followed by 0 to 3 Is)
\$                   # end of string
''', re.VERBOSE)

def to_roman(n):
'''convert integer to Roman numeral'''
if not (0 < n < 5000): # 4999まで許容
raise OutOfRangeError('number out of range (must be 1..4999)')
if not isinstance(n, int):
raise NotIntegerError('non-integers can not be converted')

result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result

# fromは変更しない
def from_roman(s):
'''convert Roman numeral to integer'''
if not s:
raise InvalidRomanNumeralError('Input can not be blank')
if not re.search(roman_numeral_pattern, s):
raise InvalidRomanNumeralError('Invalid Roman numeral: {}'.format(s))

result = 0
index = 0
for numeral, integer in roman_numeral_map:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result

In [14]:
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(FromRomanBadInput)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(RoundtripCheck)
unittest.TextTestRunner().run(test)

No traceback available to show.
..
----------------------------------------------------------------------
Ran 2 tests in 0.004s

OK
....
----------------------------------------------------------------------
Ran 4 tests in 0.006s

OK
.
----------------------------------------------------------------------
Ran 1 test in 0.166s

OK

Out[14]:
<unittest.runner.TextTestResult run=1 errors=0 failures=0>

テストケースをすべてパスした。

## リファクタリング¶

ユニットテストの最も良い点は、容赦なく リファクタリング を施す自由を与えてくれることにある。

リファクタリング とは、動作するコードを取り上げて、より良く動くようにしていくプロセスのこと。 普通、「より良い」とは「もっと速い」ということを意味するのだが、 状況によっては「メモリの使用が少ない」とか「使用ディスクスペースが小さい」とかいうことを指す場合もあるし、 あるいは単純に「よりエレガントだ」ということでもありうる。

この言葉があなたにとって、あるいはそのプロジェクトや環境においてどんな意味を持つのであれ、 リファクタリングはおよそプログラムの長期的な健全性を保つのに重要なものだと言える。

ここでは「より良い」を「より速い」と「より保守しやすい」の二つの意味 で使う。

ここにおいて「有効性の検証」は一つの辞書を参照するという作業に帰着することになる。

そして何といっても、 完全なユニットテストのセットが既に全部そろっている。 仮にモジュールのコードを半分以上変更したとしても、 ユニットテストは前のまま変わらないでいてくれる。 だから、新しいコードが元のコードと同じように機能すると —自分に対しても他人に対しても— 証明できる。

In [15]:
# roman10.py
class OutOfRangeError(ValueError):
pass
class NotIntegerError(ValueError):
pass
class InvalidRomanNumeralError(ValueError):
pass

roman_numeral_map = (('M',  1000),
('CM', 900),
('D',  500),
('CD', 400),
('C',  100),
('XC', 90),
('L',  50),
('XL', 40),
('X',  10),
('IX', 9),
('V',  5),
('IV', 4),
('I',  1)
)

to_roman_table = [ None ]
from_roman_table = {}

def to_roman(n):
'''convert integer to Roman numeral'''
if not (0 < n < 5000):
raise OutOfRangeError('number out of range (must be 1..4999)')
if int(n) != n:
raise NotIntegerError('non-integers can not be converted')
return to_roman_table[n]

def from_roman(s):
'''convert Roman numeral to integer'''
if not isinstance(s, str):
raise InvalidRomanNumeralError('Input must be a string')
if not s:
raise InvalidRomanNumeralError('Input can not be blank')
if s not in from_roman_table:
raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
return from_roman_table[s]

def build_lookup_tables():
def to_roman(n):
result = ''
for numeral, integer in roman_numeral_map:
if n >= integer:
result = numeral
n -= integer
break
if n > 0:
result += to_roman_table[n]
return result

for integer in range(1, 5000):
roman_numeral = to_roman(integer)
to_roman_table.append(roman_numeral)
from_roman_table[roman_numeral] = integer

build_lookup_tables()


コードを小さく分割してみる。

build_lookup_tables()

ここで関数が呼び出されているが、これは if __name__ == '__main__' ブロックではない。 従って、この関数はモジュールがインポートされた時に呼び出されることになる （モジュールは一度しかインポートされず、あとはキャッシュされるということをちゃんと理解しておこう。既にインポートされているモジュールを再びインポートしても、何も実行されないのだ。このコードも最初にモジュールをインポートした時にのみ呼び出されることになる）。

build_lookup_tables()関数は一体どんな処理を行うものなのか。

1. to_roman()関数は上の方でも定義されているが、これは参照テーブルで値を探しだし、 その値を返すというものでしかない。 一方、build_lookup_tables()関数は to_roman()関数を再定義していて、 この関数に（参照テーブルを加える前のコードがしていたような）実際の処理を行わせている。 この build_lookup_tables() 中で to_roman() を呼び出すと、 再定義された方の to_roman()関数が呼び出されることになる。

そして、build_lookup_tables() の実行が終わると、 この再定義されたバージョンは消えてしまう — この関数はあくまでもbuild_lookup_tables()のローカルスコープ内で定義されたもの。

to_roman_table = [ None ]
from_roman_table = {}
.
.
.
def build_lookup_tables():
def to_roman(n):                                ①
result = ''
for numeral, integer in roman_numeral_map:
if n >= integer:
result = numeral
n -= integer
break
if n > 0:
result += to_roman_table[n]
return result

for integer in range(1, 5000):
roman_numeral = to_roman(integer)          ②
to_roman_table.append(roman_numeral)       ③
from_roman_table[roman_numeral] = integer

2.再定義した to_roman()関数を呼び出してローマ数字を実際に組み立てている。

3.（再定義したto_roman()関数から）値が返ってきたら、整数と対応するローマ数字の二つを両方の参照テーブルに加えていく。

to_roman()関数は、前と同じように入力値をチェックしたあとは、単に適切な値を参照テーブルから探し出してその値を返すという処理だけを行う。

def to_roman(n):
'''convert integer to Roman numeral'''
if not (0 < n < 5000):
raise OutOfRangeError('number out of range (must be 1..4999)')
if int(n) != n:
raise NotIntegerError('non-integers can not be converted')
return to_roman_table[n]                                            ①

def from_roman(s):
'''convert Roman numeral to integer'''
if not isinstance(s, str):
raise InvalidRomanNumeralError('Input must be a string')
if not s:
raise InvalidRomanNumeralError('Input can not be blank')
if s not in from_roman_table:
raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
return from_roman_table[s]                                          ②

O(1)により、このコードは処理も速い。 現に処理速度はほとんど10倍になっている。もちろん、これは完全にフェアな比較とは言えない。このバージョンだと（参照テーブルを作り上げるので）インポートするのに長い時間がかかるからだ。 しかし、インポートされるのは一回だけなので、この起動にかかるコストはto_roman()とfrom_roman()の呼び出しごとに償却されてゆくことになる。このテストは何千回もこれらの関数を呼び出すので（往復テストだけで一万回呼び出している）、この最初のコストはすぐに回収される。

In [16]:
# test
class KnownValues(unittest.TestCase):
known_values = ((1, 'I'),
(2, 'II'),
(3, 'III'),
(4, 'IV'),
(5, 'V'),
(6, 'VI'),
(7, 'VII'),
(8, 'VIII'),
(9, 'IX'),
(10, 'X'),
(50, 'L'),
(100, 'C'),
(500, 'D'),
(1000, 'M'),
(31, 'XXXI'),
(148, 'CXLVIII'),
(294, 'CCXCIV'),
(312, 'CCCXII'),
(421, 'CDXXI'),
(528, 'DXXVIII'),
(621, 'DCXXI'),
(782, 'DCCLXXXII'),
(870, 'DCCCLXX'),
(941, 'CMXLI'),
(1043, 'MXLIII'),
(1110, 'MCX'),
(1226, 'MCCXXVI'),
(1301, 'MCCCI'),
(1485, 'MCDLXXXV'),
(1509, 'MDIX'),
(1607, 'MDCVII'),
(1754, 'MDCCLIV'),
(1832, 'MDCCCXXXII'),
(1993, 'MCMXCIII'),
(2074, 'MMLXXIV'),
(2152, 'MMCLII'),
(2212, 'MMCCXII'),
(2343, 'MMCCCXLIII'),
(2499, 'MMCDXCIX'),
(2574, 'MMDLXXIV'),
(2646, 'MMDCXLVI'),
(2723, 'MMDCCXXIII'),
(2892, 'MMDCCCXCII'),
(2975, 'MMCMLXXV'),
(3051, 'MMMLI'),
(3185, 'MMMCLXXXV'),
(3250, 'MMMCCL'),
(3313, 'MMMCCCXIII'),
(3408, 'MMMCDVIII'),
(3501, 'MMMDI'),
(3610, 'MMMDCX'),
(3743, 'MMMDCCXLIII'),
(3844, 'MMMDCCCXLIV'),
(3888, 'MMMDCCCLXXXVIII'),
(3940, 'MMMCMXL'),
(3999, 'MMMCMXCIX'),
(4000, 'MMMM'),
(4500, 'MMMMD'),
(4888, 'MMMMDCCCLXXXVIII'),
(4999, 'MMMMCMXCIX')
)

def test_to_roman_known_values(self):
'''to_roman should give known result with known input'''

for integer, numeral in self.known_values:
result = to_roman(integer)
self.assertEqual(numeral, result)

def test_from_roman_known_values(self):
'''from_roman should give known result with known input'''

for integer, numeral in self.known_values:
result = from_roman(numeral)
self.assertEqual(integer, result)

class ToRomanBadInput(unittest.TestCase):

def test_too_large(self):
'''to_roman should fail with large input'''
self.assertRaises(OutOfRangeError, to_roman, 5000)

class FromRomanBadInput(unittest.TestCase):

def test_too_many_repeated_numerals(self):
'''from_roman should fail with too many repeated numerals'''
for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
self.assertRaises(InvalidRomanNumeralError, from_roman, s)

def test_repeated_pairs(self):
'''from_roman should fail with repeated pairs of numerals'''
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
self.assertRaises(InvalidRomanNumeralError, from_roman, s)

def test_malformed_antecedents(self):
'''from_roman should fail with malformed antecedents'''
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', 'MCMC', 'XCX', 'IVI',
'LM', 'LD', 'LC'):
self.assertRaises(InvalidRomanNumeralError, from_roman, s)

# 新しく追加
def testBlank(self):
'''from_roman should fail with blank string'''
self.assertRaises(InvalidRomanNumeralError, from_roman, '')

class RoundtripCheck(unittest.TestCase):

def test_roundtrip(self):
'''from_roman(to_roman(n))==n for all n'''

for integer in range(1, 5000):
numeral = to_roman(integer)
result = from_roman(numeral)
self.assertEqual(integer, result)

In [17]:
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(FromRomanBadInput)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(RoundtripCheck)
unittest.TextTestRunner().run(test)

No traceback available to show.
..
----------------------------------------------------------------------
Ran 2 tests in 0.006s

OK
....
----------------------------------------------------------------------
Ran 4 tests in 0.011s

OK
.
----------------------------------------------------------------------
Ran 1 test in 0.027s

OK

Out[17]:
<unittest.runner.TextTestResult run=1 errors=0 failures=0>

この話の教訓は：

• 単純さは善
• 特に正規表現が関わってくる時には。ユニットテストは大規模なリファクタリングを行う自信を与えてくれる

## まとめ¶

ユニットテストは強力なコンセプトで、正しく実行できればどんな長期プロジェクトにおいても、保守にかかるコストを減らし、柔軟性も高めることができる。

ただし、ユニットテストは優れたテストケースを書くのは難しいことだし、 テストケースをたえず更新していくのには自律心が要る（とりわけ、顧客がクリティカルなバグの修正を求めてわめいている状況では）。

しかし、このユニットテストは実行可能な手法で、しかもちゃんと機能してくれるものだ。 一度このやり方がうまくいくと分かったら、ユニットテスト無しで今までどうしてやってこれたのだろうといぶかしく思うようになる。

ユニットテストのフレームワークには様々な言語向けのものが存在するし、そのどれについてもここで述べたのと同じ基本的なコンセプトを理解する必要がある：

• 明確かつ独立であり、しかも自動で実行できるようなテストケースを設計する。
• テストするコードの前にテストケースを書く
• 有効な入力値を試して、適切な結果が返ってくるかをチェックするテストを書く。
• 有効でない入力値を試して、適切に失敗するかをチェックするテストを書く。
• 新しい要件を反映するように、テストケースを書いたり修正したりする。
• 容赦なくリファクタリングを施し、機能性や拡張性、可読性、保守性、あるいは何であれコードに欠けている「～性」を向上させていく。