のーずいだんぷ

主に自分用メモですが、もしかしたら誰かの役に立つかもしれません

テスト駆動Python読んだ 〜その1〜

テスト駆動Pythonをよんだ

タイトルの通り、テストについて本格的に勉強することとした。 最初はちゃんと何ができるかを知るために書籍ベースでやるべきと判断し、唯一のPythonのテスト本である「テスト駆動Python」を読んだ

テスト駆動Python

テスト駆動Python

本書では組み込み関数のunittestではなく、サードパーティライブラリのpytestを使用して解説している。 一部解釈が曖昧なので2段くらいにしてまとめる。(もしくは今回のやつに追記する。)

1-2章まとめ

1. pytestは命名規則がある。

  • テストスクリプトの命名は[test_*]とする。
  • 関数名も[test_*]とする。
  • クラスは[Test*]とする。

2. 簡単なテスト(assertionとraises)

テストの実行

pytestは以下の様にCLIからの実行ができる。

  • pytest <テストモジュール>
assertionを使う

assert文はassertion <式>で実行し、式の評価結果がTrueになることを期待している。 - <式>は一般的な比較演算子( >,<,=等)が使用できる。実はこれがpytestの良いところの一つでunittestの場合は、例えば=の代わりにassertionEqualを使用しなければならず、個人的にはこちらの方が直感的に感じる。

  • ちなみにbooleanはassertion <変数>とかでok(おそらくnotも使えるはず)
例外のテストをraisesで行う

pytest raises(<exception | error>)を使用する。 - 実際の使用する際にはコンテクストマネージャー(withブロック)を使用する。withブロック内に例外が発生しうる処理を書く。 - また例外メッセージの抽出もできるので例外メッセージののアサーションも可能

具体的には以下のようにして記載する。

def test_sample():
    with pytest.raises(TypeError) as err_info:
        <例外が発生しうる処理>
    exception_msg = err_info.value.arg[0]   #エラーメッセージ抜き出し
    assertion exception_msg == <予想される例外メッセージ>

3. デコレータによる色々

テストシナリオ(マーキング)作成
  • @pytest.mark.smokeのように例えば設定し、コマンド実行時にpytest -m 'smoke'とすれば、このデコレータをつけたテストのみ実行される(いわゆるシナリオが作成できる。)-
フィクスチャ(テスト専用関数)の利用
  • @pytest.fixture(autouse=True) を使用することで、テスト実行を補助する関数を定義できる。例のautouse=Trueがモジュール内のすべての関数に適用されるということを示している。初期化処理等を記載するのに良さそう
テストのスキップ
  • @pytest.mark.skip@pytest.mark.skipifを使用することでデコレータをつけたテストをスキップする。skipifの方は、第一引数に条件(e.g. module.version > '1.1.0')のようにバージョンでスキップするかどうか決める等動的な判定が可能 またskip以外についてはreason引数の指定が強制となっており、-rsオプションでreasonに渡した値が表示される。
失敗ケースのテスト
  • @pytest.mark.xfail()を使用すると、失敗するであろうテストに明示することができる(結果がFAILEDになることが正解となる。)これもskipifと同様に第一引数に条件を渡すことでデコレータの適用条件を指定できる。
  • xfailによる結果の表示は、-vオプションを付けたときxfailと表示される。XPASSの表示はxfailデコレータをつけたテストが成功したことを示しており、これはpytest.inixfail_strict=Trueにすることで、FAILEDとすることができる。

4. 一部のテストだけを実行

以下の様に指定することでpytestの任意の項目のみテストができる。

  • pytest <ディレクトリ> -> ディレクトリ内に含む全てのテストを実施
  • pytest <モジュール名> -> モジュール内の全てのテスト関数、クラスを実施
  • pytest <モジュール名>::<テストメソッド | テストクラス> -> ::で指定したメソッドもしくはテストクラスに含まれる全てのメソッドを実行
  • pytest <モジュール名>::<テストクラス>::<テストメソッド> -> 指定したテストクラスに含まれるる特定のメソッドのみをテストする。

5. 色々なパラメータによるテスト

  • @pytest.mark.parametrize(<arg_name>, <[arg_list]>)でデコレーションすることで複数の引数を全てfor文でテストのパラメータを変えるが如くテストができる。

上記だと分かりにくいので例えば以下のように…

@pytest.parametrize('value', [1,2,3])
def test_type_int(value):
    assertion type(value)=="int"

valueをそのまま関数の引数に渡す(名前を変えるとエラーになる) この場合1,2,3全てに対してテストが繰り返し行われる。

  • <[arg_list]>は別に配列を格納した変数を定義して指定する形でもok
arg_list = [1,2,4]
@pytest.mark.paramtrize('value', arg_list)
def test_value_type(value):
...

例えば実行すると、以下の表示される。

$ pytest -v test_org.py 
==================================================================== test session starts ====================================================================
platform darwin -- Python 3.7.3, pytest-5.2.1, py-1.8.0, pluggy-0.13.0 -- ~/.pyenv/versions/3.7.3/envs/prac_pytest/bin/python3.7
cachedir: .pytest_cache
rootdir: ~/work/prac_pytest/src/ch2
collected 4 items                                                                                                                                           

test_org.py::test_type_int[1] PASSED                                                                                                                  [ 25%]
test_org.py::test_type_int[2] PASSED                                                                                                                  [ 50%]
test_org.py::test_type_int[3] PASSED                                                                                                                  [ 75%]
test_org.py::test_type_int[4] PASSED                                                                                                                  [100%]

===================================================================== 4 passed in 0.02s =====================================================================

結果の関数名[value] 結果valueは今回はvalueがそのまま入ってしまっているが、これは別の値を指定することもできる。 方法は2つあり、

  1. @pytest.mark.paramtrize()の引数`idsにarg_listと同じ長さの文字列の配列を渡す。
  2. arg_listの配列をpytest.param()の配列とする。

1のケースは例えば以下のように…

id_param = ['one', 'two', 'three', 'four']

@pytest.mark.parametrize('value', test_param, ids=id_param)
def test_type_int_2(value):
    assert type(value) is int

結果は…

test_org.py::test_type_int_2[one] PASSED                                                                                                              [ 25%]
test_org.py::test_type_int_2[two] PASSED                                                                                                              [ 50%]
test_org.py::test_type_int_2[three] PASSED                                                                                                            [ 75%]
test_org.py::test_type_int_2[four] PASSED  

[]の中身が変わっている!

  • ちなみに、実行時にpytest "<テストモジュール名>::<テストメソッド名>[two]"とすると、該当のパラメータだけテストできる。

2のケースでも結果は同じだが…

pytest_param = [
    pytest.param(1, id='one'),
    pytest.param(2, id='two'),
    pytest.param(3, id='three'),
    pytest.param(4, id='four')
]

@pytest.mark.parametrize('value', pytest_param)
def test_type_int_3(value):
    assert type(value) is int

これも同じ結果が出力される。

今回は一旦ここできる。 ここまででも色々なテストができると感じた。 次回はフィクスチャについてより詳しくまとめる。