概要
Python には pytest という単体テストを書く機能があり、便利なのですが、他の言語と若干仕様が異なるので、よく使う機能を備忘録としてまとめておきます。
テストの書き方・実行方法
CreateXxx
というクラスが print_aaa
というメソッドを持っている場合のテストケース。
import pytest
from xxx import CreateXxx, XxxError
class TestCreateXxx:
# 通常の評価
def test__can_print_aaa(self):
xxx = CreateXxx()
assert xxx.print_aaa(111) == 'aaa'
# エラーがでることを評価
def test__can_raise_error(self):
xxx = CreateXxx()
with pytest.raises(XxxError):
xxx.print_aaa(222)
このようなファイルを用意し、
$ pytest tests/test_create_xxx.py
と実行することで実行。
いわゆる setup 的なもの
@pytest.fixture
def init_xxx(self):
self.xxx = CreateXxx()
def test__can_print_aaa(self, init_xxx):
assert self.xxx.print_aaa() == 'aaa'
- fixture という機能を使って setup を実現します。
- 引数に fixture として定義したメソッド名を書くことでテスト開始直前に実行されることになります。
いわゆる teardown 的なもの
@pytest.fixture
def init_xxx(self):
self.xxx = CreateXxx()
yield
# ここから teardown
self.xxx.close()
def test__can_print_aaa(self, init_xxx):
assert self.xxx.print_aaa() == 'aaa'
- fixture の中で
yield
を呼ぶと、その段階でいったん実行が中断され、テストメソッド本体が実行され、その後、yield
以降の処理が実行されることになります。つまり teardown 相当の処理となります。 - 変数をローカルに閉じ込められるので可読性を高められます。
- 戻り値を戻したいときは
yield
の後ろに戻り値を書きます。 - なお、
yield
は1回しか使えません。複数書くと怒られます。
戻り値を返す、いわゆる fixture
@pytest.fixture
def xxx(self):
return CreateXxx()
def test__can_print_aaa(self, xxx):
assert xxx.print_aaa() == 'aaa'
戻り値を複数返す fixture
@pytest.fixture
def init_xxx(self):
return CreateXxx(), 'aaa'
def test__can_print_aaa(self, init_xxx):
xxx, expected = init_xxx
assert xxx.print_aaa() == expected
データジェネレート
同じテストケースを引数違いで何度も実行したい場合
@pytest.mark.parametrize を使う方法 (テストケースごとに定義)
シンプル。fixture ではないので、共有はできないし、複雑な処理もできないが、とにかくシンプルにかける。
@pytest.mark.parametrize('val, expected', [
('aa', 'bb'),
('xx', 'yy'),
])
def test__can_print_aaa(self, val, expected):
assert xxx.print_aaa(val) == expected
parametrize のテストケースタイトルが文字化けしないようにする方法
全角文字がエスケープされないように
parametrize のテストケースタイトルは扱う値の組み合わせで生成されるのだが、値が全角文字だと \u306f\u~
のようにエスケープされてどのケースなのか判別できなくなる。
pytest.ini
というファイルをルートディレクトリに置き、下記を設定することでエスケープされないようにできる:
[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
オブジェクトの内容が表示されるように
parametrize のテストケースタイトルは扱う値の組み合わせで生成されるのだが、デフォルトだとオブジェクトは hoge_obj1
のようにオブジェクト名+連番でタイトルに採用されてしまい、内容が全く分からない。
ids を指定することでカスタマイズが可能:
@pytest.mark.parametrize('aa, bb', [
(HogeObj(), FugaObj()),
...
], ids=str)
# これですべての値は str() されることになる。str の代わりに repr を指定しても良い
def my_ids(v):
if type(v) is HogeObj:
return f'{v.aaa}'
if type(v) is FugaObj:
return f'{v.bbb}'
return v
@pytest.mark.parametrize('aa, bb', [
(HogeObj(), FugaObj()),
...
], ids=my_ids)
fixture を使う方法 (複数のテストケースで共有可能)
@pytest.fixture(params=[
('aa', 'bb'),
('xx', 'yy'),
])
def case__printable(self, request) -> Tuple[str, str]:
return request.param
def test__can_print_aaa(self, case__printable):
val, expected = case__printable
assert xxx.print_aaa(val) == expected
ファイルをまたいで共通化したい場合: conftest.py
conftest.py
に書いたものはファイル間で共有される。
@pytest.fixture
def xxx():
return CreateXxx()
def test__can_print_aaa(self, xxx):
assert xxx.print_aaa() == expected
fixture の実行タイミングを明示する
デフォルトは(引数で fixture 名を指定している)テストケースメソッドの開始直前。
これを変更できる。下記のように明示する。
@pytest.fixture(scope='class')
スコープ名 | 実行されるタイミング |
---|---|
function | テストケースごとに1回実行(デフォルト) |
class | テストクラス全体で1回実行 |
module | テストファイル全体で1回実行 |
session | テスト全体で1回だけ実行 |
モック
たとえば、下記のような場合にモックしたくなる。
- AWSなど外部と繋ぐ部分(単体Testでは繋げたくない)
- 現在時、ランダム値を使う部分(毎回変わるのでテストしづらい)
- 外部の責務(=別の単体テストですでにテストしている)部分(テストのメンテで別の責務に引きずられたくない)
import される関数の中身を一時的に差し替える
- テストケース終了時に元に戻る。
mocker.patch.object で何もしないようにする
from pytest_mock import MockFixture
import hoge
class TestCreateXxx:
def test__can_print_aaa(self, mocker: MockFixture, xxx):
mocker.patch.object(hoge, 'fuga') # hoge の fuga という関数を何もしないモックに差し替える
assert xxx.print_aaa() == 'aaa'
mocker.patch.object で戻り値を定義する
from pytest_mock import MockFixture
import hoge
class TestCreateXxx:
def test__can_print_aaa(self, mocker: MockFixture, xxx):
mocker.patch.object(hoge, 'get_fuga', return_value='fuga')
assert xxx.print_aaa() == 'aaa'
mocker.patch.object で例外が発生するようにする
from pytest_mock import MockFixture
import hoge
class TestCreateXxx:
def test__can_print_aaa(self, mocker: MockFixture, xxx):
mocker.patch.object(hoge, 'get_fuga', side_effect=hoge.PiyoError('test'))
with pytest.raises(hoge.PiyoError):
xxx.print_aaa()
mocker.patch.object で関数に渡された引数をチェックする
from unittest.mock import call
import hoge
class TestCreateXxx:
def test__can_print_aaa(self, mocker: MockFixture, xxx):
m_get_fuga = mocker.patch.object(hoge, 'get_fuga', return_value='fuga')
xxx.print_aaa()
assert m_get_fuga.assert_has_calls([
call(1, 'a'), # 初回呼び出しの第1引数が 1, 第2引数が 'a'
])
mocker.patch.object で呼び出しごとに戻り値を変える
from unittest.mock import call
import hoge
class TestCreateXxx:
def test__can_print_aaa(self, mocker: MockFixture, xxx):
# side_effect で配列を渡すと呼び出しごとに順に返すようになる
m_get_fuga = mocker.patch.object(hoge, 'get_fuga', side_effect=[
'fuga1',
'fuga2',
)
xxx.print_aaa() # fuga1 が使われる
xxx.print_aaa() # fuga2 が使われる
mocker.patch.object でクラスをモック(Newした際にモックが返るように)
from unittest.mock import call
import hoge
class TestCreateXxx:
def test__can_print_aaa(self, mocker: MockFixture):
# 注意: return_value 必須
mocker.patch.object(hoge.Hoge, '__init__', return_value=None)
# 注意: 一見、static メソッドを差し替えてる風に見えるがちゃんと動く
mocker.patch.object(hoge.Hoge, 'get_fuga', return_value='fuga')
xxx.print_aaa()
assert xxx.print_aaa() == 'aaa'
環境変数を一時的に差し替える
- テストケース終了時に元に戻る。
monkeypatch.setenv で差し替える
import hoge
class TestCreateXxx:
def test__can_print_aaa(self, monkeypatch, xxx):
monkeypatch.setenv('HOGE', '1234')
assert xxx.print_aaa(111) == 'aaa'
static なプロパティを一時的に差し替える
- テストケース終了時に元に戻る。
monkeypatch.setattr で差し替える
import hoge
import MyConfig
class TestCreateXxx:
def test__can_print_aaa(self, monkeypatch, xxx):
monkeypatch.setattr(MyConfig, 'abc', 123)
以上!
随時、更新していきます。