Python
TDD

pytestを実戦投入してみた

More than 5 years have passed since last update.


特徴



  • conftest.pyを使って、セットアップコードを書くことができる(ディレクトリ毎に作成可能)

  • hook機能が豊富


    • DIっぽい機能を使って、テスト関数にデータを渡すことができる


      • テスト対象オブジェクトの生成と取得に使ったり



    • デコレーターを使って、テスト関数に値を渡すことが可能


      • 事前設定値を渡したり、テストの期待値などを渡す





  • unittestのように、テストコードを書くためのクラスを作らなくてもよい

  • 結果出力が見やすい

  • アプリリリース時に機能を削ったり、見送ったりした時にテストをスキップするのが簡単。

ディレクトリごとにconftest.pyを書くとは、こういうことです。

tests

├── __init__.py
├── event
│   ├── __init__.py
| |── conftest.py # 共通のセットアップコード
│   ├── battle_event_1207
│   │   ├── __init__.py
│   │   ├── conftest.py # 期間限定イベントのセットアップコード
│   │   └── test_event.py
│   └── battle_event_1213
│   ├── __init__.py
│   ├── conftest.py # もう一個
│   └── test_event.py
└── logics
└── __init__.py


どう使っているか

WebフレームワークはFlaskを使ってます


  • Modelのロジック部分のテスト(データストアへの書き込みは除く:コミットしないかMockを使う)

  • ユーティリティ関数部分のテスト


使ってない部分

アプリ仕様を把握したり、Webフレームワーク/テストフレームワークの使い方を覚えないと無理なところ


  • Controllerなどのリクエストが絡んだり、跨いだりする部分

  • テストデータストアを用意しないとダメな部分

  • トランザクションが絡む部分

Web TestとかFlaskの組み込みTestを使うか検討中


サンプル


セットアップコード

テスト対象のインポートとか初期化とかを行うといいようです。conftest.pyという名前でファイルを作成すれば、pytestで勝手に読み込んでくれます。

また、pytest_で始まる関数は特殊な関数で、主に、pytest_funcarg__xxxx(request)を使っています。

これを使うと、テスト関数に依存するオブジェクトなどを引数として渡すことができます。テスト関数側では、xxxxの名前を持つ引数を指定するとそこに渡されます。


conftest.py

# coding: utf-8

"""battle_event_1213 bootstrap.

テストコードで使用するモジュールの共通処理
テストターゲットの作成等

"""

from datetime import datetime, timedelta

def pytest_report_header(config):
""" テストレポートヘッダ
"""

return "battle_event_1213 テスト"

def get_time_subtract_by_delta(seconds):
return datetime.now() + timedelta(seconds=-1 * seconds)

#def pytest_addoption(parser):
# parser.addoption('--ssh', action='store', default=None, help='specify ssh host to run tests with')

class MySetup(object):
"""Factoryをここに記述
"""

def getEventCls(self):
from event_models.battle_event_1213 import Event
return Event

def makeEvent(self, *args, **kwargs):
return self.getEventCls()(*args, **kwargs)

def pytest_funcarg__mysetup(request):
"""DI(依存性注入)
テスト関数/メソッドの引数に`mysetup`を指定する
"""

return MySetup()

def pytest_funcarg__event(request):
return MySetup().makeEvent()



テストコード

pytest.mark.skipifデコレーターを使うと、条件が真のときテストをスキップすることが可能です。もう削除した機能だけど、一応テストケースだけ残しておいたり、期間限定の機能は、有効期限を指定して時間が来たらスキップするという使い方をすることが可能です。

セットアップコード側で、pytest_funcarg__event(request)という関数で返されたオブジェクトは、event引数に渡されます。テスト対象オブジェクトは基本最初からセットアップされている状態テストしたいので、conftest.pyを使えばわざわざテストコード側でセットアップする手間がなくなります。

この例のように、pytest.mark.parametrizeデコレーターを使って、入力値や期待値を渡すことで、テスト関数/メソッドを繰り返し記述する手間をなくすことができます。pytest_funcarg__xxxx(request)と同じように、テスト関数の引数に渡されます。


test_event.py

# coding: utf-8

import pytest
from datetime import datetime

# テスト有効期間
is_timeout_event = datetime.now() > datetime(2012,12,19,23,59)
check_event_period = pytest.mark.skipif('%s' % is_timeout_event)

# テストパラメータ
EXCEEDED_GAUGE = 140
MAX_GAUGE = 100
MAX_ATTACK_PT = 4

# skipifに何か文字を入れればテストをスキップする(ダメな機能を消したのでテストスキップする)
@pytest.mark.skipif('"ゲージはすごいゲージを使うのでこのテストをスキップする"')
class TestEventDame(object):
@pytest.mark.parametrize('prop', [
'dame_gauge',
])
def test_exists_properties(self, event, prop):
assert hasattr(event, prop)

@pytest.mark.parametrize(('input_val', 'set_val', 'mode'), [
(0, 0, False),
(1, 1, False),
(MAX_GAUGE, MAX_GAUGE, True),
(EXCEEDED_GAUGE, MAX_GAUGE, True),
])
def test_update_dame_gauge(self, event, input_val, set_val, mode):
event.update_dame_gauge(input_val)
assert event.dame_gauge == set_val
assert event.is_dame() is mode

@pytest.mark.parametrize(('value', 'expected'), [
(0, 0),
(1, 1),
(2, 2),
(3, 3),
(4, 6),
])
def test_get_attack_pt_effect(self, event, value, expected):
result = event.get_attack_pt_effect(value)
assert result == expected

# 期間終了したら、このテストをスキップする
@check_event_period
class TestEventSugoiGauge(object):
@pytest.mark.parametrize('prop', [
'attack_pt',
'current_use_attack_pt',
'update_datetime',
'_use_full_attack_pt_count',
'_use_full_attack_pt_count_with_win',
])
def test_exists_properties(self, event, prop):
assert hasattr(event, prop)

@pytest.mark.parametrize(('value', 'expected'), [
(0, False),
(1, False),
(2, False),
(3, False),
(4, True),
])
def test_is_max_attack_pt(self, event, value, expected):
assert event.is_max_attack_pt(value) is expected

@pytest.mark.parametrize(('value', 'expected'), [
(0, 1),
(1, 2),
(9, 10),
])
def test_inc_use_full_attack_pt_count(self, event, value, expected):
event._use_full_attack_pt_count = value
event._use_full_attack_pt_count_with_win = value
event.inc_use_full_attack_pt_count()
event.inc_use_full_attack_pt_count_with_win()
assert event._use_full_attack_pt_count == expected
assert event._use_full_attack_pt_count_with_win == expected

@pytest.mark.parametrize(('value', 'win_flag', 'expected', 'win_expected'), [
(0, True, 0, 0),
(1, True, 0, 0),
(3, True, 0, 0),
(MAX_ATTACK_PT, False, 1, 0),
(MAX_ATTACK_PT, True, 1, 1),
])
def test_set_use_full_attack_pt(self, event, value, win_flag, expected, win_expected):
event.set_use_full_attack_pt(value, is_win=win_flag)
assert event._use_full_attack_pt_count == expected
assert event._use_full_attack_pt_count_with_win == win_expected

if __name__ == '__main__':
pytest.main()



結論

unittestには戻れない


参考