現在地: ホーム Dive Into Python 3

難易度: ♦♦♦♦♢

リファクタリング

鍵盤を何度も弾き鳴らし、幾度と無く弾きあかした果てに、簡明さが芸術における無上の報償として姿を現す。
フレデリック・ショパン

 

飛び込む

好むと好まざるとにかかわらず、バグは発生する。どれだけ手を尽くして包括的なユニットテストを書いても、バグは出てくるものだ。「バグ」とは一体何だろうか? バグとはまだ書かれていないテストケースのことだ。

>>> import roman7
>>> roman7.from_roman('') 
0
  1. これはバグだ。空白文字列に対しては、有効なローマ数字となっていない他の文字列の場合と同じように、InvalidRomanNumeralError例外が送出されなくてはならない。

バグを再現できたら、それを修正するのに先立って、パスしないようなテストケースを書かなければならない。こうしてバグをはっきりと示すのだ。

class FromRomanBadInput(unittest.TestCase):
    .
    .
    .
    def testBlank(self):
        '''from_roman should fail with blank string'''
        self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, '') 
  1. これは非常にシンプルなコードだ。すなわち、from_roman()を空白文字列とともに呼び出し、InvalidRomanNumeralError例外が送出されるかどうかを確認する。難しいのはバグを見つけ出す段階であって、一度分かってしまえば、そのバグをテストするのはごく簡単なことなのだ。

コードにバグがあって、さらにそのバグに対応するテストケースもあるのだから、このテストケースは失敗するはずだ:

you@localhost:~/diveintopython3/examples$ python3 romantest8.py -v
from_roman should fail with blank string ... FAIL
from_roman should fail with malformed antecedents ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

======================================================================
FAIL: from_roman should fail with blank string
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest8.py", line 117, in test_blank
    self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, '')
AssertionError: InvalidRomanNumeralError not raised by from_roman

----------------------------------------------------------------------
Ran 11 tests in 0.171s

FAILED (failures=1)

これでようやくこのバグを修正できる。

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

    result = 0
    index = 0
    for numeral, integer in romanNumeralMap:
        while s[index:index+len(numeral)] == numeral:
            result += integer
            index += len(numeral)
    return result
  1. たった二行のコードを追加するだけで足りる。一行目で明示的に空白文字列をチェックし、二行目にはraise文をあてるのだ。
  2. この本の中でまだ触れてないことだと思うので、これを文字列のフォーマットの最後のレッスンとしよう。Python 3.1からは、フォーマット指定子の中でインデクスを使用する場合に数字を省略できるようになっている。つまり、format()メソッドの最初の引数を参照する場合に、フォーマット指定子の{0}を使わなくとも、単純に{}とすればPythonが自動で適当なインデクスを埋めてくれるのだ。これは引数がいくつあっても機能するもので、最初の{}{0}に、次の{}{1}というように解釈されていく。
you@localhost:~/diveintopython3/examples$ python3 romantest8.py -v
from_roman should fail with blank string ... ok  
from_roman should fail with malformed antecedents ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

----------------------------------------------------------------------
Ran 11 tests in 0.156s

OK  
  1. 空白文字列のテストケースをパスしていることから、このバグが修正されたと分かる。
  2. 他のテストケースもすべてパスしているので、このバグ修正のせいでどこかが壊れたりはしていないということが分かる。コーディングの手を止めよう。

このようにコーディングしても、別にバグが修正しやすくなるわけではない。(このバグのように)簡単なバグには簡単なテストケースで足りるが、複雑なバグには複雑なテストケースが必要となるだろう。もしかしたら、テスト中心の開発環境だとバグを修正するのに長い時間がかかるように見えるかもしれない。まずもって、(テストケースを書いて)何がバグなのかをコードの中で明確に示してからバグを修正しなくてはならないし、しかもそこでテストケースをパスしなかったら、修正が間違っていたのか、それともテストケース自体にバグがあるのかを調べなくてはならないからだ。しかし、長い目で見れば、テストコードとそのテストを受けるコードの間を行ったり来たりすることは十分割に合うことだろう。こうすれば、バグが最初の一回で正しく修正されやすくなるし、新しいテストケースと一緒に他のすべてのテストケースを再実行することも簡単にできるので、新しいコードを加える際に古いコードを壊してしまうということが非常に少なくなるからだ。今日のユニットテストは、明日の回帰テストとなるのだ。

要件の変更に対処する

どれだけ顧客を地面に釘付けにして、ハサミやら熱いロウやらを使ったおぞましい罰を与えるぞと脅かし、厳密な要件を引き出しても、要件というのは変わってしまうものだ。そもそも、ほとんどの顧客は実際に見てみるまで、自分が何を求めているのかを理解しないものだ。たとえ理解していたとしても、顧客はどういうものがあれば十分なのかをはっきりと伝えるのが上手ではない。仮に明確に伝えてきたとしても、どのみち次のリリースの時には別の機能も欲しがるようになっている。だから、要件の変更に合わせてテストケースをアップデートできるようにしておこう。

例えば、このローマ数字の変換関数が扱える値の範囲を拡張したいと思ったとしよう。普通なら、ローマ数字のどの文字も連続して四回以上繰り返すことはできないのだが、実のところローマ人は時に「4000を表すために、Mの文字を四個続けることができる」という例外を設けていた。この変更を施せば、変換できる数の範囲を1..3999から1..4999に拡張できることになる。しかし、まず最初にテストケースに修正を加える必要がある。

[roman8.pyをダウンロードする]

class KnownValues(unittest.TestCase):
    known_values = ( (1, 'I'),
                      .
                      .
                      .
                     (3999, 'MMMCMXCIX'),
                     (4000, 'MMMM'),                                      
                     (4500, 'MMMMD'),
                     (4888, 'MMMMDCCCLXXXVIII'),
                     (4999, 'MMMMCMXCIX') )

class ToRomanBadInput(unittest.TestCase):
    def test_too_large(self):
        '''to_roman should fail with large input'''
        self.assertRaises(roman8.OutOfRangeError, roman8.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(roman8.InvalidRomanNumeralError, roman8.from_roman, s)

    .
    .
    .

class RoundtripCheck(unittest.TestCase):
    def test_roundtrip(self):
        '''from_roman(to_roman(n))==n for all n'''
        for integer in range(1, 5000):                                    
            numeral = roman8.to_roman(integer)
            result = roman8.from_roman(numeral)
            self.assertEqual(integer, result)
  1. すでにタプルに入っている既知の値については何も変更を加えないが(これらはまだテストする意味のある値だ)、4000台の数をいくつか加える必要がある。ここでは4000(最も短い数)、4500(二番目に短い数)、4888(最も長い数)、4999(最大の数)を加えてある。
  2. 「大きな入力値」の定義が変わっている。このテストはto_roman()4000と共に呼び出してエラーが出るかを確認するものだったが、4000-4999は有効な入力値となったので、引数の値を5000まで引き上げなくてはならない。
  3. 「繰り返され過ぎている数字」の定義も変わっている。このテストはfrom_roman()'MMMM'とともに呼び出してエラーが出るかを確認するものだったが、MMMMは有効なローマ数字となったので、引数の値を'MMMMM'まで引き上げなくてはならない。
  4. この動作確認テストでは1から3999までのすべての数をループしていたが、値の範囲が拡張されたのに合わせて、このforループも4999までの数をテストするように修正しなければならない。

これでテストケースは新しい要件に追いついたが、コードの方はまだ修正していないので、テストケースのいくつかは失敗するはずだ。

you@localhost:~/diveintopython3/examples$ python3 romantest9.py -v
from_roman should fail with blank string ... ok
from_roman should fail with malformed antecedents ... ok
from_roman should fail with non-string input ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ERROR          
to_roman should give known result with known input ... ERROR            
from_roman(to_roman(n))==n for all n ... ERROR                          
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

======================================================================
ERROR: from_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest9.py", line 82, in test_from_roman_known_values
    result = roman9.from_roman(numeral)
  File "C:\home\diveintopython3\examples\roman9.py", line 60, in from_roman
    raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
roman9.InvalidRomanNumeralError: Invalid Roman numeral: MMMM

======================================================================
ERROR: to_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest9.py", line 76, in test_to_roman_known_values
    result = roman9.to_roman(integer)
  File "C:\home\diveintopython3\examples\roman9.py", line 42, in to_roman
    raise OutOfRangeError('number out of range (must be 0..3999)')
roman9.OutOfRangeError: number out of range (must be 0..3999)

======================================================================
ERROR: from_roman(to_roman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest9.py", line 131, in testSanity
    numeral = roman9.to_roman(integer)
  File "C:\home\diveintopython3\examples\roman9.py", line 42, in to_roman
    raise OutOfRangeError('number out of range (must be 0..3999)')
roman9.OutOfRangeError: number out of range (must be 0..3999)

----------------------------------------------------------------------
Ran 12 tests in 0.171s

FAILED (errors=3)
  1. from_roman()の既知の値のテストは'MMMM'に突き当たったところで失敗する。from_roman()がまだこのローマ数字を無効なものとしているからだ。
  2. to_roman()の既知の値のテストは4000に突き当たったところで失敗する。to_roman()がまだこれを範囲外の数としているからだ。
  3. 往復テストも4000に突き当たったところで失敗する。to_roman()がまだこれを範囲外の数としているからだ。

これで新しい要件によって失敗するテストケースができ上がったので、これらのテストをパスするようにコードを修正する作業に移ることができる(ユニットテストをコーディングし始めた最初のうちは、テスト対象のコードが決してユニットテストの「先」にこないことに違和感を覚えるかもしれない。コードが追いついていないのに、まだやるべき作業があって、そしてコードがテストケースに追いついたら、即座にコーディングをやめる。一旦これに慣れたら、これまでどうやってテスト無しでプログラムしてたのかと不思議に思うようになっていることだろう)。

[roman9.pyをダウンロードする]

roman_numeral_pattern = re.compile('''
    ^                   # beginning of string
    M{0,4}              # thousands - 0 to 4 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 to_roman(n):
    '''convert integer to Roman numeral'''
    if not (0 < n < 5000):                        
        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

def from_roman(s):
    .
    .
    .
  1. from_roman()関数には何の修正も加える必要はなく、roman_numeral_patternを変更をするだけで良い。よく注意してコードを見れば、正規表現の一番初めのところで、任意につけられるMの文字の最大数が3個から4個に変えられていることに気がつくだろう。この修正によって、3999ではなく4999に対応するローマ数字も扱えるようになる。それというのも、from_roman()自体は完全に一般的な関数で、ローマ数字がどれだけ繰り返されているかといったことには構わず、ただ繰り返し使われているローマ数字を洗い出して足し合わせていくだけのものだったからだ。これ以前に'MMMM'を扱えなかったのは、単にこの正規表現パターンでマッチさせて明示的に除外していたというだけのことでしかない。
  2. to_roman()には入力値の範囲をチェックする部分に小さな修正を一つ加えるだけで良い。0 < n < 4000とチェックしていた部分を、0 < n < 5000とチェックするように書き直したのだ。そして、有効な入力値の範囲を変更したのにあわせて、raiseを使って送出するエラーメッセージも変更しておいた(1..3999から1..4999に)。関数の後の部分は何も変更する必要はなく、もう既に新しいケースを扱えるようになっている(この関数は何の問題もなく、千ごとに'M'を付け加えてくれる。例えば、4000を渡せば、この関数は'MMMM'を返してくれるだろう。これ以前にそうならなかったのは、入力値の範囲をチェックする段階で明示的に除外していたからだ)。

たったこの二つ修正だけでちゃんと動くようになるとは信じられないかもしれないが、ほら、私の言葉なんて信じなくても良いから、自分で確かめてみてほしい。

you@localhost:~/diveintopython3/examples$ python3 romantest9.py -v
from_roman should fail with blank string ... ok
from_roman should fail with malformed antecedents ... ok
from_roman should fail with non-string input ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

----------------------------------------------------------------------
Ran 12 tests in 0.203s

OK  
  1. テストケースをすべてパスした。コーディングを止めよう。

包括的なユニットテストがあれば、「僕を信じてくれよ」なんていうプログラマに依りかからなくてすむのだ。

リファクタリング

包括的なユニットテストの最大の利点は、ようやくすべてのテストケースをパスできたときに味わえる感情にあるのではないし、あなたのせいでコードが壊れたと言い立ててきた人に対して、そうではないと実際に証明できたときに得られる感覚にあるわけでもない。ユニットテストの最も良い点は、容赦なくリファクタリングを施す自由を与えてくれることにあるのだ。

リファクタリングとは、動作するコードを取り上げて、より良く動くようにしていくプロセスのことだ。普通、「より良い」とは「もっと速い」ということを意味するのだが、状況によっては「メモリの使用が少ない」とか「使用ディスクスペースが小さい」とかいうことを指す場合もあるし、あるいは単純に「よりエレガントだ」ということでもありうる。この言葉があなたにとって、あるいはそのプロジェクトや環境においてどんな意味を持つのであれ、リファクタリングはおよそプログラムの長期的な健全性を保つのに重要なものだと言える。

ここでは「より良い」を「より速い」と「より保守しやすい」の二つの意味で使う。要するに、from_roman()関数は私が期待するよりも遅くて複雑になってしまっているということなのだが、その原因は巨大で扱いにくい正規表現を使ってローマ数字の有効性を検証していることにある。ここまで読むと、あなたはこのように考えるかもしれない: 「確かにこの正規表現は大きくて不調法だけど、それ以外に任意の文字列が有効なローマ数字かどうかを検証する方法があるだろうか?」

答え: たった5000個しかないんだから、参照テーブルを作ってみたらどうだろう? しかも、こうすれば正規表現をまったく使う必要がないということに気がつけば、このアイデアはさらに魅力なものになるだろう。つまり、整数をローマ数字に変換する参照テーブルを組み立てるときに、ローマ数字を整数に変換する逆の参照テーブルも作ることができるので、任意の文字列が有効なローマ数字かどうかを判別する時には、有効なローマ数字の一覧表が出来上がっていることになるのだ。ここにおいて「有効性の検証」は一つの辞書を参照するという作業に帰着することになる。

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

[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文がまったく無いことに気がつくだろう。これはif __name__ == '__main__'ブロックではないのだ。従って、この関数はモジュールがインポートされた時に呼び出されることになる(モジュールは一度しかインポートされず、あとはキャッシュされるということをちゃんと理解しておこう。既にインポートされているモジュールを再びインポートしても、何も実行されないのだ。このコードも最初にモジュールをインポートした時にのみ呼び出されることになる)。

それで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
  1. これはかなり巧妙なプログラムの組み方だ……もしかしたら巧妙すぎるぐらいかもしれない。to_roman()関数は上の方でも定義されているが、これは参照テーブルで値を探しだし、その値を返すというものでしかない。一方、build_lookup_tables()関数はto_roman()関数を再定義していて、この関数に(参照テーブルを加える前のコードがしていたような)実際の処理を行わせている。このbuild_lookup_tables()中でto_roman()を呼び出すと、再定義された方のto_roman()関数が呼び出されることになる。そして、build_lookup_tables()の実行が終わると、この再定義されたバージョンは消えてしまう — この関数はあくまでもbuild_lookup_tables()のローカルスコープ内で定義されたものなのだ。
  2. ここでは、再定義したto_roman()関数を呼び出してローマ数字を実際に組み立てている。
  3. (再定義した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]                                          
  1. 前と同じように入力値をチェックしたあとは、to_roman()関数は単に適切な値を参照テーブルから探し出してその値を返すという処理だけを行う。
  2. 同様に、from_roman()関数も入力値をチェックする部分の他に一行のコードが置かれているだけになっている。ここには正規表現は使われていないし、ループもない。あるのはローマ数字と整数を相互変換するO(1)のコードだけだ。

これでちゃんと動くのかって? もちろん、これでちゃんと動く。証明してみせよう。

you@localhost:~/diveintopython3/examples$ python3 romantest10.py -v
from_roman should fail with blank string ... ok
from_roman should fail with malformed antecedents ... ok
from_roman should fail with non-string input ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

----------------------------------------------------------------------
Ran 12 tests in 0.031s                                                  

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

このお話の教訓は何かって?

まとめ

ユニットテストは強力なコンセプトで、正しく実行できればどんな長期プロジェクトにおいても、保守にかかるコストを減らし、柔軟性も高めることができる。ただし、ユニットテストは万能薬でも魔法の問題解決機でも銀の弾丸でもないということは理解しておいてほしい。優れたテストケースを書くのは難しいことだし、テストケースをたえず更新していくのには自律心が要る(とりわけ、顧客がクリティカルなバグの修正を求めてわめいている状況では)。加えて、ユニットテストは、機能テストや統合テスト、アセプタンステストなどの他の形式のテストに取って代わるものでもない。しかし、このユニットテストは実行可能な手法で、しかもちゃんと機能してくれるものだ。一度このやり方がうまくいくと分かったら、ユニットテスト無しで今までどうしてやってこれたのだろうといぶかしく思うようになることだろう。

ここまでの数章ではかなり広い話題を扱ってきたが、その多くの部分はPythonに特有なものというわけではなかった。実際に、ユニットテストのフレームワークには様々な言語向けのものが存在するし、そのどれについてもここで述べたのと同じ基本的なコンセプトを理解する必要がある:

© 2001– Mark Pilgrim
© Fukada, Fujimoto(日本語版)