pytest 使い方まとめ

pythonのテストフレームワークであるpytestの主だった使い方をまとめてみました
2018.09.07

西田@大阪です

pythonのテストフレームワークであるpytestの主だった使い方をまとめてみました

今回の記事で利用したバージョンは以下です

  • Python: 3.6.5
  • pytest: 3.8.0
  • freezegun: 0.3.10

テスト対象

ファイルはtestから始まるファイルが対象になります

test_sample.py

メソッドはtestから始まる関数が対象になります

def test_something():
    # ~~~

複数のテストをまとめたいときはclassを使いグルーピングしたいテストをそのクラスのメソッドとして記述します

class TestGroup(object):
    def test_something_1():
        # ~~~
    def test_something_2():
        # ~~~

assertion

assert キワードのあとに bool 値を返す式を書きます

a = 1
assert 1 == a

例外のテスト

特定の例外を送出することを確認するのにはpytest.raisesを使います

def test_exception():
    with pytest.raises(ValueError)
        raise ValueError()

mark

markを使ってテストにメタデータを付与することができます

テストをスキップ(ペンディング)したい場合は @pytest.mark.skip を使います

@pytest.mark.skip
def test_skip():
    # ~~~

複数のパターンのパラメーターをテストする場合には @pytest.mark.parametrizeが使えます。第1引数にテスト関数で使うための変数名をカンマ区切りで指定し、第2引数に配列でパラメーターを指定します

@pytest.mark.parametrize("x,y,sum", [
    (1, 2, 3),
    (2, 3, 5),
])
def test_add(x, y, sum):
    assert (x + y) == sum

fixture

テストに必要なオブジェクトを提供するのにfixtureは使えます

@pytest.fixtureでデコレートした関数の関数名と同じ名前の引数をテスト関数に設定して使います

class Client(object):
    def send(self, msg):
        # ...

@pytest.fixture()
def client():
    return Client('connect info')

def test_sample(client):
    client.send('message')
    # ...

fixtureをつかってテストの前後に処理を挿入することもできます

class Client(object):
    def connect(self):
        # ...

    def send(self, msg):
        # ...

    def close(self):
        # ...

@pytest.fixture()
def client():
    # -- 前処理 
    client = Client()
    client.connect()

    # テスト実行
    yield client

    # -- 後処理
    client.close()

def test_sample(client):
    client.send('message')
    # ...

複数のテストにまとめてfixtureを設定したい場合はクラスに @pytest.mark.usefixturesを設定します

@pytest.fixture()
def sample_fixture():
    # ...

@pytest.mark.usefixtures('sample_fixture')
class TestSample(object):
    def test_sample_1(self):
        # 引数に設定しなくてもsample_fixtureが実行前によばれる

    def test_sample_2(self):
        # 引数に設定しなくてもsample_fixtureが実行前によばれる

グローバルで使える共通のfixtureを作成した場合は conftest.pyに記述します

# ./conftest.py
@pytest.fixture
def global_fixture():
    # ...

# ./test_sample.py
def test_sample(global_fixture):
    # ...

conftest.pyは置かれたディレクトリ以下のすべてのテストで有効になります

-- tests/
  |-- conftest.py
  |-- test_sample_1.py
  |-- sub/
  |    |-- conftest.py
  |    |-- test_sample_2.py

上記の例ですと

  • test_sample_1.py の中のテストは test/conftest.pyが有効です
  • sub/test_sample_2.pyの中のテストはtest/conftest.pytest/sub/conftest.pyが有効になります

またautouseをTrueに設定することで、引数やデコレーターで指定をしなくても自動で実行されるfixtureを作ることができます

autouseTrueのfixtureをconftest.pyに書くことによって、conftest.pyが有効な範囲のテストに前後処理を追加することできます

下記はscopeにfunctionを設定しすべてのテスト実行前に処理を追加している例です

# ./conftest.py
@pytest.fixture(scope="function", autouse=True)
def setup():
    # ...

# ./test_sample.py
def test_sample_1():
    # テスト実行前にsetup()が呼ばれる

def test_sample_2():
    # テスト実行前にsetup()が呼ばれる

Mock

monkeypatch fixture を使えば既存の関数をモックすることができます

※ ただし datetime.datetime.now などCで書かれている組み込み型はこの方法ではモックできません。

class MockTeapotResponse:
    def getcode(self):
        return 418

def test_mock(monkeypatch):
    def mock_urlopen(*_):
        return MockTeapotResponse()

    monkeypatch.setattr(request, 'urlopen', mock_urlopen)
    res = request.urlopen('http://www.example.com')
    assert res.getcode() == 418

時間を操作

monkeypatch fixtureを使っても datetime.datetime.now をMockして時間を操作することはできませんが、 freezegunを使うことで時間を操作してテストを行えます

from datetime import datetime
from freezegun import freeze_time

def test_freeze_time():
    now = datetime.now()

    with freeze_time(now) as frozen_time:
        assert datetime.now() == now
        # tickで時間を進める
        frozen_time.tick(timedelta(seconds=1))
        assert datetime.now() == now + timedelta(seconds=1)

まとめ

いかがでしたでしょうか?

標準のunittestよりも簡潔にかけるので、これからpythonでテストを書くときは積極的に使っていきたいと思います

参考