pytestを実戦投入してみた


##特徴

- `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には戻れない

##参考

- [pytest のドキュメントを翻訳しました](http://d.hatena.ne.jp/t2y-1979/20120608/1339092961)

- [Pytestへようこそ](http://pytest.org/latest-ja/)