この章では、整数とローマ数字を相互変換する一組のユーティリティ関数を書いて、 それをデバッグする。 ローマ数字の組み立て方とその有効性の検証法については、 「ケーススタディ: ローマ数字」で学んだ。
では一度そこに立ち返って、どうやったらそこから双方向なユーティリティを作れるかを考える。
ローマ数字の規則を見ると、興味深い洞察がいくつか得られる:
では、roman.py
モジュールに何が必要なのかを考えていこう。
主たる関数として to_roman()
と
from_roman()
の二つがいる。
to_roman()
関数は1から3999までの整数を引数にとり、
対応するローマ数字を文字列として返す。
to_roman()
関数が望み通りの動きをするかをチェックするテストケースを書く。
大丈夫、読み間違えでもなんでもない。
実際にこれから、まだ書かれてもいないコードをテストするコードを書く。
これは テスト駆動開発 (TDD) と呼ばれている手法。
変換を行うこの二つの関数 — to_roman()
と後で出てくる from_roman()
は、
これらをインポートする他のもっと大規模なプログラムとは独立に、一つの構成単位として書いたりテストしたりできる。
Pythonには、ユニットテストのためのフレームワーク unittest
モジュール がある。
ユニットテスト は、テストを中心に据える開発手法全般のかなめといえる存在。 ユニットテストを書くなら、早い段階で書きあげた上で、コードや要件の変更に合わせてアップデートしていくことが重要になる。 多くの人はコードを書く前にテストを書くやり方を推奨していて、 実際にこの章でもこのスタイルを用いるが、 ユニットテスト自体はいつ書いても有益なものである。
ユニットテストを書こうとすれば、要件を実用的な形で練り上げざるをえなくなる。
ユニットテストがあれば過剰なコーディングを避けることができる。 すべてのテストケースに通ったなら、その関数はもう完成。
新しいコードが古いバージョンのコードと同じように動くかどうかを確認するのに役立つ。
誰かがあなたの所にやって来て「お前が加えた変更のせいで古いコードが壊れたじゃないか」とわめいた時に、ユニットテストがあれば言い逃れができる。
包括的なテストスイートがあれば、 あなたのコードが別の誰かのコードを壊してしまうといったことが非常に少なくなる。 事前に他の人のユニットテストを走らせることができる。 (コードスプリントでこういうやり方をしているのを見たことがある。 チームに割り当てられた仕事を分割した上で、 各々が仕様書に目を通してユニットテストを書き、 それをチームで共有する。 こうすると、他の部分とうまくかみ合わないようなコードが早いうちに直されるようになる)
一つのテストケースは、そのコードに関するただ一つの問いのみに答える。 また、テストケースというものは
ユニットテストの本質は自動化にある。
ある関数がテストをパスしたかどうかを、人の解釈によらず、自ら判定するものでなくてはならない。
(同じ関数をテストしているテストケースも含む)
他のいかなるテストケースからも独立し、 単体で動作するものでなくてはならない。
以上のことを前提に、一番最初の要件に対するテストケースを書いてみる。
to_roman()
は、1から3999までのすべての整数に対応するローマ数字を返せなくてはならない。一見すると、「このコードって本当に何かの役に立つの?」と思われるかもしれない。
一つのクラスが定義されているが、
中には __init__
メソッドが入っていない。
別のメソッドもあることはあるのだが、
このメソッドは一度も呼び出されていない。
このスクリプトの __main__
ブロックにしても、
このクラスやメソッドを参照してすらいない。
テストケースを書くには、
まずunittest
モジュールのTestCase
クラスをサブクラス化(継承)する。
このクラスには、テストケースで特定の条件をテストする時に便利なメソッドがたくさん入っている。
known_values
は、整数/数字のペアのリスト。
このリストには、最も小さい10個の数、最も大きい数、一文字のローマ数字で表される数すべて、さらにこれら以外の有効な数字からランダムに抽出された数が含まれている。
ありとあらゆる入力値を試す必要はないが、
境界事例だと分かっているものについてはすべてテストするべき。
import unittest
unittest
<module 'unittest' from 'C:\\Miniconda3\\lib\\unittest\\__init__.py'>
import roman1
import unittest
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')
)
def test_to_roman_known_values(self):
'''to_roman should give known result with known input'''
for integer, numeral in self.known_values:
result = roman1.to_roman(integer)
self.assertEqual(numeral, result)
if __name__ == '__main__':
unittest.main()
個々のテストはそれぞれメソッド(def test_to_roman_known_values()
)として表される。
メソッドは引数を取らなければ値も返さないようなものであり、
メソッド名は4文字のtestで始まっていなければならない。
もし、メソッドが
def test_to_roman_known_values()
の中:
最後に残ったステップは、 正しい値が返されているかどうかチェックすること。
これはテストケース一般でチェックされる項目なので、TestCaseクラスに二つの値が等しいかどうかを調べるassertEqual
メソッドが用意されている。
to_roman()が返した値(result)が、
返されるべき既知の値(numeral)と一致しなかったなら、assertEqualは例外を送出し、テストは失敗する。
二つの値が等しければ、assertEqualは何もしない。だから、to_roman()のすべての戻り値が、返されるべき既知の値と一致したならば、assertEqualは一度も例外を送出せず、test_to_roman_known_valuesは正常に終了する。
テストケースができたら、 to_roman()関数のコードを書き始めることができる。 だが、まずは最初に中身が空のto_roman()関数を作って、 テストが失敗することを確かめなくてはならない。
もし、何も書いていないのにパスできてしまうようなら、 何のテストにもならない。 通らないようなテストを書いて、パスするまでコーディングする。
この段階では、to_roman()
のAPIを定義するだけで、
中身をコーディングしようとは思ってはいない
(まずはテストを失敗させなくてはならない)。
この場合にはPythonの予約語のpassを使えばいい。
これは実行されても、全く何の処理も行わない。
# roman1.py
def to_roman(n):
'''convert integer to Roman numeral'''
pass
コマンドラインでromantest1.py を起動してテストを実行する。
コマンドラインのオプションとして-v
をつければ、それぞれのテストケースが実行される際の処理の状況が詳しく出力されるようになる。
> python3 romantest1.py -v
test_to_roman_known_values (__main__.KnownValues) ①
to_roman should give known result with known input ... FAIL ②
======================================================================
FAIL: to_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest1.py", line 73, in test_to_roman_known_values
self.assertEqual(numeral, result)
AssertionError: 'I' != None ③
----------------------------------------------------------------------
Ran 1 test in 0.016s ④
FAILED (failures=1) ⑤
このスクリプトを走らせるとunittest.main()
が呼び出され、
それぞれのテストケースが実行される。
各々のテストケースはromantest.pyの中のクラスに入っているメソッド。
このテストクラスはどのような構成をしていてもよい。いくつかのクラスがあって、それぞれにテストメソッドが一つずつ入っているというのでもよいし、複数のメソッドが入ったクラスが一つあるというのでも構わない。ただすべてのテストクラスが unittest.TestCase
を継承してさえいればいい。
2行目に、各々のテストについて、unittest
モジュールはそのメソッドの docstring
とテストの成否を出力する。予想通り、このテストケースは失敗している。
失敗したテストケースについては、unittest
は何が起こったのかをトレースして表示してくれる。
このケースでは、assertEqual()
を呼び出した際に AssertionError
が送出されている。to_roman(1)
は 'I' を返すものとされていたのに、
そうならなかったから
(この関数にはreturn文が置かれて無いので、Python のNull値にあたるNone が返されている)。
それぞれのテストの詳細を出力した後で、unittest
はいくつのテストが実行され、それにどれくらいの時間がかかったのかを表示する。
まとめると、少なくとも一つのテストケースをパスしなかったので、 このテストは失敗したことになる。
なお、テストケースにパスしなかったという場合について、 unittestは
を区別する。
Failure とは、assertEqualやassertRaisesといったassertXYZメソッドを呼びだしたが、表明された条件が真でなかったり、期待通りに例外が送出されなかったせいで失敗してしまった場合を指す。
これ以外の、テストしているコードやユニットテストのテストケース自体から送出された例外はすべてErrorとされる。
import unittest
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')
)
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)
TestLoader
と TextTestRunner
を使う。
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
No traceback available to show. F ====================================================================== FAIL: test_to_roman_known_values (__main__.KnownValues) to_roman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-3-f4de237d18b0>", line 66, in test_to_roman_known_values self.assertEqual(numeral, result) AssertionError: 'I' != None ---------------------------------------------------------------------- Ran 1 test in 0.002s FAILED (failures=1)
<unittest.runner.TextTestResult run=1 errors=0 failures=1>
import roman1
import unittest
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')
)
def test_to_roman_known_values(self):
'''to_roman should give known result with known input'''
for integer, numeral in self.known_values:
result = roman1.to_roman(integer)
self.assertEqual(numeral, result)
各々のテストについて、unittestモジュールはそのメソッドのdocstringとテストの成否を出力する。予想通り、このテストケースは失敗している。
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
No traceback available to show. F ====================================================================== FAIL: test_to_roman_known_values (__main__.KnownValues) to_roman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-5-6f3417485f53>", line 67, in test_to_roman_known_values self.assertEqual(numeral, result) AssertionError: 'I' != None ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1)
<unittest.runner.TextTestResult run=1 errors=0 failures=1>
%psource roman1.to_roman
import inspect
inspect
<module 'inspect' from 'C:\\Miniconda3\\lib\\inspect.py'>
print(inspect.getsource(roman1.to_roman))
def to_roman(n): '''convert integer to Roman numeral''' pass
roman_numeral_map
はタプルのタプルで、次の三つのものを定めている。
すなわち、
この内側のタプルはすべて(numeral, value)
というペアからなっている。
ちなみに、ここでは一文字のローマ数字だけではなく、
CM(「1000引く100」)など二文字のものについても定めている。
こうすることで、to_roman()
関数のコードがより簡潔なものになる。
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)
)
def to_roman(n):
'''convert integer to Roman numeral'''
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
roman_numeral_map
にデータを詰め込んだことが功を奏していて、
引き算を用いて数を表すルールを扱うための特別なロジックを組む必要がなくなっている。
つまり、ローマ数字に変換するには次のようにすればいいのだ。
まず、単純に roman_numeral_map
をイテレートして、
入力値以下の数の中で最大の整数値を探す。
次に、そのような数が見つかったら、 対応するローマ数字を出力値の末尾に付け加え、 さらに入力値からその数を引く。 後はこれを繰り返し、また繰り返してさらに繰り返せばいい。
to_roman()関数がどのように動くのかまだよく分からないのなら、print()をwhileループの末尾につけてみるといい:
def to_roman(n):
'''convert integer to Roman numeral'''
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
print('subtracting {0} from input, adding [{1}] to output, at least n:{2}'.format(integer, numeral, n))
return result
デバッグ用のprint()文をつけると、次のように出力されるようになる:
to_roman(1424)
subtracting 1000 from input, adding [M] to output, at least n:424 subtracting 400 from input, adding [CD] to output, at least n:24 subtracting 10 from input, adding [X] to output, at least n:14 subtracting 10 from input, adding [X] to output, at least n:4 subtracting 4 from input, adding [IV] to output, at least n:0
'MCDXXIV'
少なくとも、この手作業の抜き取り検査ではうまくやっているように見える。だが、さっき書いたテストケースはパスするだろうか?
def to_roman(n):
'''convert integer to Roman numeral'''
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
import unittest
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')
)
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)
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
No traceback available to show. . ---------------------------------------------------------------------- Ran 1 test in 0.003s OK
<unittest.runner.TextTestResult run=1 errors=0 failures=0>
to_roman()
関数は「既知の値」のテストケースに通った。
このテストはありうるケースすべてを試しているわけではないが、この関数を多様な入力値、例えば、一文字のローマ数字に対応するすべての数、最大の数(3999)、最も長いローマ数字に対応する数(3888)などでテストしている。
だから、この関数は適切な入力値ならどれでもうまく処理できると考えても差し支えないだろう。 じゃあ「不適切な値」を入力したらどうなるのか。
適切な入力を与えて成功するかをテストするだけでは十分ではなく、 適切でない入力を与えられた時に失敗することも検証しなくてはならない。 それもどんな失敗でも良いというわけではなく、狙った通りに失敗しなくてはならない。
明らかにこれは意図していない戻り値. そもそも、ちゃんとしたローマ数字ですらない! 実を言うと、ここにある数はすべて入力値の制限範囲を越えているのだが、それでも関数はとりあえず戻り値をでっちあげている。
こっそり不適当な値を返すというのはものすごーく悪いことだ。 どうせ落ちるプログラムなら、早いうちに騒々しく落ちてくれた方がずっと良い。 言い習わされているように「Halt and catch fire」(中断の後、発火せよ)というやつだ。 Pythonでは、例外の送出が Halt and Catch Fire の役目を果たす。
to_roman(4000)
'MMMM'
to_roman(5000)
'MMMMM'
to_roman(9000)
'MMMMMMMMM'
考えるべき問題は「どうやったらこれをテストできる条件として表せられるのだろう?」ということ。取っ掛かりとして、まず、:
to_roman()
関数は、3999より大きい整数を与えられたら OutOfRangeError
を送出しなければならない。
前のテストケースと同じように、unittest.TestCase
を継承したクラスを作る。
一つのクラスに複数のテストを入れてもいいのだが、
ここでは新しいクラスを作ることにする。
このテストは先ほどのテストとは性質が異なったもの。
適切な入力値に対するテストを一つのクラスにまとめて、 不適切な入力値に対するテストはまた別のクラスにまとめる。
unittest.TestCase
クラスには、
assertRaises
メソッドが用意されているが、これは次のような引数をとるもの。
すなわち、送出されるべき例外、テスト対象の関数、そして関数に渡す引数
(テスト対象の関数が複数の引数をとる場合には、それらの引数を順に並べてassertRaisesに渡せばいい。そうすれば、そのまま関数に渡してくれる)。
def to_roman(n):
'''convert integer to Roman numeral'''
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
class ToRomanBadInput(unittest.TestCase):
def test_too_large(self):
'''to_roman should fail with large input'''
self.assertRaises(OutOfRangeError, to_roman, 4000)
to_roman()
を直接呼び出して、特定の例外を送出しているかどうかを
(try...except
ブロックを使って)自ら調べなくても、
assertRaises
メソッドがこの処理を全部カプセル化してくれている。
だから、どんな例外が送出されるべきなのか(roman2.OutOfRangeError)と、対象となる関数(to_roman()
)、そして関数がとる引数(4000)を渡すだけでよい。
後は、assertRaises
メソッドがto_roman()
を呼び出して、
OutOfRangeError
が送出されるかどうかをチェックしてくれる。
%tb
test = unittest.TestLoader().loadTestsFromTestCase(ToRomanBadInput)
unittest.TextTestRunner().run(test)
No traceback available to show. E ====================================================================== ERROR: test_too_large (__main__.ToRomanBadInput) to_roman should fail with large input ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-19-4ae99e2f06fb>", line 5, in test_too_large self.assertRaises(OutOfRangeError, to_roman, 4000) NameError: name 'OutOfRangeError' is not defined ---------------------------------------------------------------------- Ran 1 test in 0.002s FAILED (errors=1)
<unittest.runner.TextTestResult run=1 errors=1 failures=0>
「Fail」ではなく、代わりに「Error」が返ってくる。 これは微妙な差だが、違いは大きい。
実は、ユニットテストには
の三つの戻り値がある。
テストしたモジュールの中に OutOfRangeError
という名前の例外が存在しなかった。
思い出して欲しいのだが、この例外は範囲外の入力値が渡された時に送出されるべき例外としてassertRaises()
メソッドに渡したものだった。
しかし、そもそもこの例外は存在しなかったので、assertRaises()
メソッドの呼び出しが失敗してしまったのだ。
ここでは結局、to_roman()
関数がテストされることはなかった。そこに行き着きさえしなかった。
この問題を解決するためには、OutOfRangeError
例外を定義する必要がある。
例外とはクラスでできている。
「Out of Range
」エラーは、Value Error
の一種だと言える
— 引数の値が受け取ることのできる範囲を越えている。
だから、この例外は組み込みのValueError
例外を継承している。
これは絶対に必要というわけではない(基底クラスのExceptionを継承してもいい)
class OutOfRangeError(ValueError):
pass
%tb
test = unittest.TestLoader().loadTestsFromTestCase(ToRomanBadInput)
unittest.TextTestRunner().run(test)
No traceback available to show. F ====================================================================== FAIL: test_too_large (__main__.ToRomanBadInput) to_roman should fail with large input ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-19-4ae99e2f06fb>", line 5, in test_too_large self.assertRaises(OutOfRangeError, to_roman, 4000) AssertionError: OutOfRangeError not raised by to_roman ---------------------------------------------------------------------- Ran 1 test in 0.002s FAILED (failures=1)
<unittest.runner.TextTestResult run=1 errors=0 failures=1>
Error
が返されることもなく、その代わりにテストは失敗(Failure
)している。
これは、assertRaises()
メソッドが正しく呼び出され、
実際にto_roman()
関数がこのユニットテストフレームワークにテストされたということを意味している。
これで、このテストをパスするためのコードを書くことができる。
与えられた入力値(n)が3999より大きければ、
OutOfRangeError
例外を送出する。
ちなみに、このユニットテストは、例外と一緒に出力されるエラー文字列については何もチェックしていないが、これ用のテストはまた別に書くことができる (ただし、文字列の多言語化の問題に注意すること。 この問題はユーザーの言語や環境に依存する)。
class OutOfRangeError(ValueError):
pass
def to_roman(n):
'''convert integer to Roman numeral'''
if n > 3999:
raise OutOfRangeError('number out of range (must be less than 4000)')
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
これでテストをパスするようになっただろうか?
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(ToRomanBadInput)
unittest.TextTestRunner().run(test)
No traceback available to show. . ---------------------------------------------------------------------- Ran 1 test in 0.002s OK . ---------------------------------------------------------------------- Ran 1 test in 0.002s OK
<unittest.runner.TextTestResult run=1 errors=0 failures=0>
テストを二つともパスした。 テストとコーディングの間を行ったり来たりしながら、繰り返し作業したので、 テストが「Fail」から「Pass」になったのは、さっき書いた二行のコードのおかげだと確信できる。 この種の確信を得るのは大変だが、そのコードが使われる年月を総じれば、 きっとその元はとれる。
ローマ数字は0や負の数を表すことはできない
to_roman(0)
''
to_roman(-1)
''
それぞれの条件に対するテストを付け加える。
新しいテストのtest_zero()
メソッド:
unittest.TestCase
の中に定められているassertRaises()
メソッドを用いて、
to_roman()
を0を引数として呼び出し、
適切な例外(OutOfRangeError
)が送出されるかどうかを確認している。
test_negative()
メソッド:
-1
をto_roman()
に渡しているというだけの違いしかない。
この新しいテストのいずれかにおいてOutOfRangeErrorが送出されなかった場合(その原因としては、関数が実際の値を返したか別の例外を返したかのどちらか)には、
テストは失敗したものとみなされる。
class ToRomanBadInput(unittest.TestCase):
def test_too_large(self):
'''to_roman should fail with large input'''
self.assertRaises(OutOfRangeError, to_roman, 4000)
def test_zero(self):
'''to_roman should fail with 0 input'''
self.assertRaises(OutOfRangeError, to_roman, 0)
def test_negative(self):
'''to_roman should fail with negative input'''
self.assertRaises(OutOfRangeError, to_roman, -1)
テストが失敗することを確かめよう:
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(ToRomanBadInput)
unittest.TextTestRunner().run(test)
No traceback available to show. . ---------------------------------------------------------------------- Ran 1 test in 0.002s OK F.F ====================================================================== FAIL: test_negative (__main__.ToRomanBadInput) to_roman should fail with negative input ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-27-1ea3a47efc08>", line 12, in test_negative self.assertRaises(OutOfRangeError, to_roman, -1) AssertionError: OutOfRangeError not raised by to_roman ====================================================================== FAIL: test_zero (__main__.ToRomanBadInput) to_roman should fail with 0 input ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-27-1ea3a47efc08>", line 8, in test_zero self.assertRaises(OutOfRangeError, to_roman, 0) AssertionError: OutOfRangeError not raised by to_roman ---------------------------------------------------------------------- Ran 3 tests in 0.005s FAILED (failures=2)
<unittest.runner.TextTestResult run=3 errors=0 failures=2>
予想通りどちらのテストも失敗している。さて、今度はコードに戻って、どうすればテストをパスできるようになるのかを考えてみよう。
この部分ではPython流のショートカットが上手に使われていて、
複数の比較演算子が同時に評価されている。
意味的には
if not ((0 < n) and (n < 4000))
と同じだが、
こちらの方が読みやすい。
この一行のコードで、大きすぎる数、負の数、ゼロの三種類の入力値すべてが補足される。
条件を変えたら、人が読む用のエラー文字列も修正するのを忘れないように。
def to_roman(n):
'''convert integer to Roman numeral'''
if not (0 < n < 4000):
raise OutOfRangeError('number out of range (must be 1..3999)')
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(ToRomanBadInput)
unittest.TextTestRunner().run(test)
No traceback available to show. . ---------------------------------------------------------------------- Ran 1 test in 0.003s OK ... ---------------------------------------------------------------------- Ran 3 tests in 0.004s OK
<unittest.runner.TextTestResult run=3 errors=0 failures=0>
数字をローマ数字に変換する際の機能要件にはもう一つ、整数以外の数の扱いがある。
to_roman(0.5)
''
値をでっちあげて返してしまっている。
to_roman(1.0)
'I'
整数でない数をテストするのは難しいことではない。 まず、NotIntegerError例外を定義する。
class NotIntegerError(ValueError):
pass
次に、NotIntegerError
例外が送出されるかどうかをチェックするテストケースを書く。
class ToRomanBadInput(unittest.TestCase):
def test_too_large(self):
'''to_roman should fail with large input'''
self.assertRaises(OutOfRangeError, to_roman, 4000)
def test_zero(self):
'''to_roman should fail with 0 input'''
self.assertRaises(OutOfRangeError, to_roman, 0)
def test_negative(self):
'''to_roman should fail with negative input'''
self.assertRaises(OutOfRangeError, to_roman, -1)
def test_non_integer(self):
'''to_roman should fail with non-integer input'''
self.assertRaises(NotIntegerError, to_roman, 0.5)
テストがちゃんと失敗するか確かめる。
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(ToRomanBadInput)
unittest.TextTestRunner().run(test)
No traceback available to show. . ---------------------------------------------------------------------- Ran 1 test in 0.002s OK .F.. ====================================================================== FAIL: test_non_integer (__main__.ToRomanBadInput) to_roman should fail with non-integer input ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-34-7868efa3e185>", line 16, in test_non_integer self.assertRaises(NotIntegerError, to_roman, 0.5) AssertionError: NotIntegerError not raised by to_roman ---------------------------------------------------------------------- Ran 4 tests in 0.006s FAILED (failures=1)
<unittest.runner.TextTestResult run=4 errors=0 failures=1>
テストをパスするようなコードを書く。
組み込みの isinstance()
関数は、
変数が特定の型(厳密に言えば、ここにはその型を継承した型も含む)かどうかを調べる。
引数のnが int
でなかったら、さっき新しく作った
NotIntegerError
を送出する。
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
最後に、このコードで本当にテストにパスするようになったかをチェックする。
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(ToRomanBadInput)
unittest.TextTestRunner().run(test)
No traceback available to show. . ---------------------------------------------------------------------- Ran 1 test in 0.003s OK .... ---------------------------------------------------------------------- Ran 4 tests in 0.007s OK
<unittest.runner.TextTestResult run=4 errors=0 failures=0>
ローマ数字の文字列を整数に直すのは、整数をローマ数字に直すことよりも難しいように見える。
もちろん、ここには有効なローマ数字かどうかのチェックの問題が存在する。 ある整数が0より大きいかどうかをチェックするのは簡単だが、 ある文字列が有効なローマ数字かどうかをチェックするのは少し難しいことだ。 しかし、私たちは既にローマ数字をチェックする正規表現を作成している。 だから、この部分はもう完成している。
文字列をどう変換するかという問題自体はまだ残っているが、すぐ後で見るように、
ローマ数字と整数値との対応関係を詰め込んだデータ構造があるおかげで、from_roman()
関数のコードの核は to_roman()
関数と同じくらい単純なものになる。
最初はテスト。 ここでは正確に変換されているかどうかを抜き取り検査する「既知の値」のテストが必要だろう。 テストスイートには既に既知の値の対応表が入っているので、こいつを再利用する。
import unittest
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')
)
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()
関数と from_roman()
関数は互いを逆にしたもので、
前者は整数を特定の形式の文字列に変換し、後者は特定の形式の文字列を整数に変換する。
理論的には、数を「往復」させることができなくてはならない。
つまり、to_roman()
関数に渡して文字列に直し、
続いてその文字列をfrom_roman()
関数に渡して整数に直した場合に、
最初の数が戻ってこなくてはならない。
対称性を用いて、1..3999
のすべての値に対して to_roman()
を呼び出し、
さらに to_roman()
で変換を施して、
出力された値が元の入力値と同じかをチェックするテストケースを作ることができる。
class RoundtripCheck(unittest.TestCase):
def test_roundtrip(self):
'''from_roman(to_roman(n))==n for all n'''
for integer in range(1, 4000):
numeral = to_roman(integer)
result = from_roman(numeral)
self.assertEqual(integer, result)
from_roman()
をまったく定義していないので、実行してもエラーが送出されるだけ。
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(ToRomanBadInput)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(RoundtripCheck)
unittest.TextTestRunner().run(test)
No traceback available to show. E. ====================================================================== 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-38-fbe19e69e68a>", line 74, in test_from_roman_known_values result = from_roman(numeral) NameError: name 'from_roman' is not defined ---------------------------------------------------------------------- Ran 2 tests in 0.004s FAILED (errors=1) .... ---------------------------------------------------------------------- Ran 4 tests in 0.006s OK E ====================================================================== ERROR: test_roundtrip (__main__.RoundtripCheck) from_roman(to_roman(n))==n for all n ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-39-15de0fba6e80>", line 8, in test_roundtrip result = from_roman(numeral) NameError: name 'from_roman' is not defined ---------------------------------------------------------------------- Ran 1 test in 0.002s FAILED (errors=1)
<unittest.runner.TextTestResult run=1 errors=1 failures=0>
とりあえず空の関数を定義すれば、この問題は解決する。
def from_roman(s):
'''convert Roman numeral to integer'''
この関数はドキュメンテーション文字列のみで定義されているのだ。これはPythonでは正式に認められていることだ。実際に、これを推奨しているプログラマもいる。「スタブするな、ドキュメントせよ!」)
このテストケースはこれで実際に失敗するようになったはずだ。
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(ToRomanBadInput)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(RoundtripCheck)
unittest.TextTestRunner().run(test)
No traceback available to show. F. ====================================================================== FAIL: test_from_roman_known_values (__main__.KnownValues) from_roman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-38-fbe19e69e68a>", line 75, in test_from_roman_known_values self.assertEqual(integer, result) AssertionError: 1 != None ---------------------------------------------------------------------- Ran 2 tests in 0.007s FAILED (failures=1) .... ---------------------------------------------------------------------- Ran 4 tests in 0.005s OK F ====================================================================== FAIL: test_roundtrip (__main__.RoundtripCheck) from_roman(to_roman(n))==n for all n ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-39-15de0fba6e80>", line 9, in test_roundtrip self.assertEqual(integer, result) AssertionError: 1 != None ---------------------------------------------------------------------- Ran 1 test in 0.002s FAILED (failures=1)
<unittest.runner.TextTestResult run=1 errors=0 failures=1>
それではfrom_roman()関数を書く。
def from_roman(s):
"""convert Roman numeral to integer"""
result = 0
index = 0
for numeral, integer in roman_numeral_map:
# M
# 1000:M
# s[0:0+1]
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
while s[index:index+len(numeral)] == numeral:
この部分のパターンは to_roman()
と同じで、
ローマ数字のデータ構造(タプルからなるタプル)をイテレートしていっている。
ただし、前のコードではできる限り大きな整数値にマッチするようにしていたが、ここではできる限り「最大の」ローマ数字の文字列にマッチするようにしている。
from_roman()
関数がどのように動いているのかよく分からなかったら、
print
文をwhileループの末尾につけてみるといい:
def from_roman(s):
"""convert Roman numeral to integer"""
result = 0
index = 0
for numeral, integer in roman_numeral_map:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
print('found', numeral, 'of length', len(numeral), ', adding', integer)
return result
from_roman('MCMLXXII')
found M of length 1 , adding 1000 found CM of length 2 , adding 900 found L of length 1 , adding 50 found X of length 1 , adding 10 found X of length 1 , adding 10 found I of length 1 , adding 1 found I of length 1 , adding 1
1972
テストをもう一度実行する。
def from_roman(s):
"""convert Roman numeral to integer"""
result = 0
index = 0
for numeral, integer in roman_numeral_map:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
# print('found', numeral, 'of length', len(numeral), ', adding', integer)
return result
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(ToRomanBadInput)
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.007s OK . ---------------------------------------------------------------------- Ran 1 test in 0.140s OK
<unittest.runner.TextTestResult run=1 errors=0 failures=0>
ここから興味深いことが二つ分かる。
from_roman()
は適切な入力値に関しては(少なくとも全ての既知の値については)うまく動くということ。
この関数が「往復」テストもパスしている。
既知の値に関するテスト結果と合わせると、to_roman()とfrom_roman()は適切な値ならどんなものについても正しく処理できると考えてもよさそうだ。
(ただし絶対ではない。
理論上は、特定の入力値を間違ったローマ数字に変換するバグがto_roman()に存在していて、かつ from_roman()
にも対応するバグが存在し、
to_roman()
が誤って生成したローマ数字を元の入力値に誤変換しているということもありうる。アプリケーションや要件によってはこの可能性が問題になるかもしれないが、
その場合には問題が解決するまでテストケースをより包括的なものにしていけばよい)。
これで from_roman()
は適切な入力値をうまく処理できるようになった。
それでは最後のパズルのピース — 不適切な入力値の処理の問題—に取り組むとしようか。
この問題は結局のところ、
文字列を調べて有効なローマ数字かどうかを判断する方法を見つけ出すことに帰着する。これは to_roman()
関数の数字の入力値をチェックする処理よりも本質的に難しいものだ。
しかし、私たちには意のままに使える強力なツールがある。
そう、正規表現だ(正規表現になじみがないなら、この折に正規表現の章を読んでおいて欲しい)。
M、 D、 C、 L、 X、 V、Iの文字を使ってローマ数字を組み立てる場合には、いくつかの単純なルールがある。ルールを見直してみよう:
時に文字は足し算のように働く。Iは1、IIは2、そしてIIIは3だ。VIは6で(文字通り「5と1」だ)、VIIは7、VIIIは8。
10の文字(I、X、C、M)は三回まで繰り返せる。4については、次の5の文字から引いて表さなければならない。つまり、4をIIIIと表すことはできず、代わりにIVとしなければならないのだ(「5引く1」)。40はXLと書かれ(「50引く10」)、41はXLI、42はXLII、43はXLIII、そして44はXLIVと表せられる(「50引く10と5引く1」)。
時に文字は……足し算とは逆の役割を果たす。ある文字を他の文字の前に置くと、後の文字から値を引いたことになるのだ。例えば、9を作るには、次の10の数から引き算をしなければならない。つまり、8はVIIIだが、9はIXとなり(「10引く1」)、VIIIIとは書けないのだ(なぜならIの文字を4回繰り返すことはできないから)。90はXC、900はCMとなる。
5の文字は繰り返すことができない。10は必ずXと表し、VVとすることはできない。100もCであって、LLとはならない。
ローマ数字は左から右に読むので、文字の並べ方が非常に重要になる。DCは600だが、CDはそれとは全く異なる数を表すのだ(400、「500引く100」)。また、CIは101だがICは適切なローマ数字ですらない(1を直接100から引くことはできないからだ。代わりにXCIXと書かなくてはならない、「100引く10、加えて10引く1」)。
使えそうなテストとしては、ある数字が繰り返し使われすぎているような文字列をfrom_roman()
に渡した場合に例外が送出されるかどうか確かめるというのがあるだろう。
どれぐらい繰り返されていれば多すぎると言えるのかは、その数字によって異なる。
class ToRomanBadInput(unittest.TestCase):
def test_too_large(self):
'''to_roman should fail with large input'''
self.assertRaises(OutOfRangeError, to_roman, 4000)
def test_zero(self):
'''to_roman should fail with 0 input'''
self.assertRaises(OutOfRangeError, to_roman, 0)
def test_negative(self):
'''to_roman should fail with negative input'''
self.assertRaises(OutOfRangeError, to_roman, -1)
def test_non_integer(self):
'''to_roman should fail with non-integer input'''
self.assertRaises(NotIntegerError, to_roman, 0.5)
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, roman6.from_roman, s)
他にも、繰り返すことのできない特定のパターンをチェックするのもテストとして有用だろう。例えば、IXは9だがIXIXはローマ数字として有効ではない。
class ToRomanBadInput(unittest.TestCase):
def test_too_large(self):
'''to_roman should fail with large input'''
self.assertRaises(OutOfRangeError, to_roman, 4000)
def test_zero(self):
'''to_roman should fail with 0 input'''
self.assertRaises(OutOfRangeError, to_roman, 0)
def test_negative(self):
'''to_roman should fail with negative input'''
self.assertRaises(OutOfRangeError, to_roman, -1)
def test_non_integer(self):
'''to_roman should fail with non-integer input'''
self.assertRaises(NotIntegerError, to_roman, 0.5)
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, roman6.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, roman6.from_roman, s)
三つ目のテストでは、数字が正しい順番、つまり大きいものから小さいものへと並んでいるかでチェックしよう。
例えば、CLは150を表すが、LCは有効な数字ではない。 50を表す数字が100を表す数字の前にくることはないからだ。このテストには不適切な数字が前に来ているパターンをランダムに選んで入れるとしよう。Mの前にIがあるとか、Xの前にVがあるとかいう場合だ。
class ToRomanBadInput(unittest.TestCase):
def test_too_large(self):
'''to_roman should fail with large input'''
self.assertRaises(OutOfRangeError, to_roman, 4000)
def test_zero(self):
'''to_roman should fail with 0 input'''
self.assertRaises(OutOfRangeError, to_roman, 0)
def test_negative(self):
'''to_roman should fail with negative input'''
self.assertRaises(OutOfRangeError, to_roman, -1)
def test_non_integer(self):
'''to_roman should fail with non-integer input'''
self.assertRaises(NotIntegerError, to_roman, 0.5)
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)
これら三つのテストはすべてfrom_roman()がInvalidRomanNumeralErrorという新しい例外を送出することを前提としているが、この例外はまだ定義されていない。
class InvalidRomanNumeralError(ValueError):
pass
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(ToRomanBadInput)
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 F..F.F. ====================================================================== FAIL: test_malformed_antecedents (__main__.ToRomanBadInput) from_roman should fail with malformed antecedents ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-49-096381ac8b39>", line 35, in test_malformed_antecedents self.assertRaises(InvalidRomanNumeralError, from_roman, s) AssertionError: InvalidRomanNumeralError not raised by from_roman ====================================================================== FAIL: test_repeated_pairs (__main__.ToRomanBadInput) from_roman should fail with repeated pairs of numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-49-096381ac8b39>", line 28, in test_repeated_pairs self.assertRaises(InvalidRomanNumeralError, from_roman, s) AssertionError: InvalidRomanNumeralError not raised by from_roman ====================================================================== FAIL: test_too_many_repeated_numerals (__main__.ToRomanBadInput) from_roman should fail with too many repeated numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-49-096381ac8b39>", line 22, in test_too_many_repeated_numerals self.assertRaises(InvalidRomanNumeralError, from_roman, s) AssertionError: InvalidRomanNumeralError not raised by from_roman ---------------------------------------------------------------------- Ran 7 tests in 0.011s FAILED (failures=3) . ---------------------------------------------------------------------- Ran 1 test in 0.118s OK
<unittest.runner.TextTestResult run=1 errors=0 failures=0>
後は有効なローマ数字かをチェックする正規表現をfrom_roman()関数に組み込めばいい。
import re
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
再びテストを実行する
%tb
test = unittest.TestLoader().loadTestsFromTestCase(KnownValues)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(ToRomanBadInput)
unittest.TextTestRunner().run(test)
test = unittest.TestLoader().loadTestsFromTestCase(RoundtripCheck)
unittest.TextTestRunner().run(test)
No traceback available to show. .. ---------------------------------------------------------------------- Ran 2 tests in 0.007s OK ....... ---------------------------------------------------------------------- Ran 7 tests in 0.010s OK . ---------------------------------------------------------------------- Ran 1 test in 0.174s OK
<unittest.runner.TextTestResult run=1 errors=0 failures=0>