先日の読書に影響されて、ユニットテストっていうものを調べてみたぜ。えーとまとめると以下のような感じか?
  • スクリプトはたくさんの関数が集まってできてるじゃん?
  • 大雑把にいえば、関数が全部バグなしだったら全体もバグなしってことになるじゃん?
  • ユニットテストは関数単位でテストすること。いろんなパターンの引数で関数を呼び出しまくって、返り値が正しいかチェックしまくる。
  • これをスクリプト書いてる最中に定期的に実行してれば、どっかのタイミングで関数が壊れたらスグわかる!

なるほどって感じだ。これまで、作った関数のチェックといえば、実際に使うところでちょろっと呼び出して結果を print するくらいだったもんね。全体のリファクタリングをやるときも有効そう。以下目次。
とりあえず pip しとく
テスト対象
ユニットテストファイル
カバレッジ



とりあえず pip しとく
  • $ pip install nose
  • $ pip install coverage
  • $ pip install mock
  • $ pip install parameterized



テスト対象。foo.py って名前にしといた。この中のメソッドをユニットテストしていくぜ。
class Foo:

    # test_1 と test_2 で扱うよー。
    @staticmethod
    def static_method(x, y):
        return x + y

    # test_3 と test_4 で扱うよー。
    @staticmethod
    def exception_method(x):
        if x is True:
            raise TypeError()
        elif x is False:
            raise ValueError()
        elif x == 1:
            return x
        else:
            return None

    # test_5 で扱うよー。
    @staticmethod
    def use_other_class_method():
        return Bar.static_method()


class Bar:

    # test_5 で扱うよー。
    @staticmethod
    def static_method():
        return 1

    # test_7 で扱うよー。
    X = 1

    # test_7 で扱うよー。
    @classmethod
    def class_method(cls):
        return cls.X



ユニットテストファイル。foo_test.pyって名前にしといた。テストファイルの名前は test_ か _test をつけないとダメ。とりあえず使う予定の import を並べとく。
from nose.tools import ok_, eq_, with_setup
from mock import Mock
from parameterized import parameterized

import foo  # これはテストするモジュール。

実例1: 基本的なテスト。Foo.static_methodをテストする。
def test_1():

    # Input.
    x = 1
    y = 2

    # Expected.
    expected = 3

    # Verify.
    actual = foo.Foo.static_method(x, y)
    eq_(expected, actual)

実例2: たくさんの入力値、期待値パターンを一気に書く。ぶっちゃけ実例1は忘れて全部コレでよさげ。
@parameterized([
    (1, 2, 3),
    (3, 4, 7),
    (6, 3, 9),
])
def test_2(x, y, expected):

    # Verify.
    actual = foo.Foo.static_method(x, y)
    eq_(expected, actual)

実例3: 例外をテスト。Foo.exception_methodをテストする。raises っていうアノテーションを使う方法もあったけれど、それと parameterized を組み合わせる方法がよくわかんなかったからコレで。
@parameterized([
    (True, TypeError),
    (False, ValueError),
])
def test_3(x, expectedException):

    try:
        foo.Foo.exception_method(x)
    except Exception as e:
        ok_(isinstance(e, expectedException))
        return
    ok_(True is False, 'Exception must be thrown.')

実例4: parametarized に例外を含む。実例2,3の合成。例外を出すかもしれないし普通に返り値が来るかもしれないメソッドをテストする。このかたちが今回の完成形って感じがする。あとは必要に応じて後述のモックを使えばよいか。
@parameterized([
    (True, True, TypeError),
    (False, True, ValueError),
    (1, False, 1),
    ('a', False, None),
])
def test_4(x, expectException, expected):

    # 通常の返り値期待。
    if not expectException:
        actual = foo.Foo.exception_method(x)
        eq_(expected, actual)
        return

    # 例外期待。
    try:
        foo.Foo.exception_method(x)
    except Exception as e:
        ok_(isinstance(e, expected))
        return
    ok_(True is False, 'Exception must be thrown.')

実例5: メソッドをすげ替える(モックする)
def test_5():

    # 対象メソッド内の Bar.static_method をモックします。
    # フツーに実行したら 1 が返ってくるけど、そこを 5 にすげ替える。
    foo.Bar.static_method = Mock(return_value=5)

    # Expected.
    expected = 5

    # Verify.
    actual = foo.Foo.use_other_class_method()
    eq_(expected, actual)

実例6: モジュールの前後に関数を実行させる
def setup():
    '''setupって名前で作れば自動で実行。'''
    print('実例6: setup')
def teardown():
    '''teardownって名前で作れば自動で実行。'''
    print('実例6: teardown')

実例7: 関数の前後に関数を実行させる
def setup2():
    foo.Bar.X = 5
def teardown2():
    foo.Bar.X = 1

@with_setup(setup2, teardown2)
def test_7():
    eq_(5, foo.Bar.class_method())

実行は下記コマンドを打てばよい。test_、_testのついたファイル、関数を全部自動実行してくれる。あるいはテストファイル内に実行スクリプトを書いてもよいぞ。
$ nosetests -v -s --with-coverage

# テストファイル内に書く場合。
nose.main()
テストをちゃんとクリアしたかどうかズラッと出てくれる。vがテスト名の表示、sが標準出力の表示、with-coverageがカバレッジの出力。



カバレッジ。このカバレッジってやつが楽しくてな。テストがスクリプトをすみずみまで舐め回したかどうか見せてくれるんだよ。下記コマンド実行で閲覧用htmlを出力してくれる。
$ coverage html




こんなんが出る。楽しすぎるじゃん。今回は Bar.static_method は実行してないから(モック例に使ったため)舐め回し不足として赤くなってる。わかりやすいな。
注意事項として、テスト実行に
nose.main()
を使うとカバレッジが中途半端に赤くなる。具体的にはスクリプトの定義文部分。本当なら定義とかを全部終わらす前に網羅率の計測をしなくちゃならないんだが、スクリプト内に実行文を書いちゃうと定義処理が終わってから計測を開始することになっちゃってダメなんだろう。スクリプト内からのテスト実行も可能って話だったが、実際はターミナルから実行したほうが無難かもしんない。



オッケー終了! わりと実用的にまとまった気がする。


以下の記事からリンクされています