LoginSignup
90
79

More than 5 years have passed since last update.

pytestでテストケースの前後処理をする

Last updated at Posted at 2017-07-02

はじめに (TL;DR)

初投稿です。どうぞよろしくお願いします。
さて、今回はpytestで前後処理の記述方法について紹介します。
Javaっとした世界でいうところのbeforeClassやafterClass等ですね。
目的としてはこんなところでしょうか。

  1. 前後処理などセットアップコードを共通化したい
  2. setup処理からparameterを引き渡したい
  3. テストケースごとに固有の前後処理をしたい

では、ゴタックーを並べるのはその辺にして、早速見ていきましょう。

構成

pytestの世界では、セットアップコードをconftest.pyと呼ばれる別ファイルに外だしするのがお作法なようです。
従って、ディレクトリは例えば以下のように切ります。

tests
├── __init__.py
└── sample
    ├── __init__.py
    |── conftest.py  # セットアップコードを記述
    └── test_code.py # テストケースを記述

セットアップコードのモジュールはconftest.pyという名前にしないとpytestが拾ってくれません。
テストケースのモジュールはtest_*.pyのような名前にしないと、同じくpytestが拾ってくれません。
モジュールに限らず、testと接頭辞をつける系の小ネタはpythonテスト界隈の全般的なお作法なようですね(Javaのspock時は割とフリーダムな名前をつけていたことを思い出しながら)。
さて、以上踏まえて、個別のモジュールを見ていきたいと思います。

コーディング

はい、ドーン!といきたいところですが、実はやり方は2つあるようです。

  • yield命令を使う
  • addfinalizer命令を使う

ですが、今回はとてもシンプルなyield命令を使うことにします。
先ほどの目的順に見ていきましょう。

前後処理などセットアップコードを共通化したい

では、ドーン!

conftest.py
import pytest


@pytest.fixture(scope='session', autouse=True)
def scope_session():
    print("setup before session")
    yield
    print("teardown after session")


@pytest.fixture(scope='module', autouse=True)
def scope_module():
    print("    setup before module")
    yield
    print("    teardown after module")


@pytest.fixture(scope='class', autouse=True)
def scope_class():
    print("        setup before class")
    yield
    print("        teardown after class")


@pytest.fixture(scope='function', autouse=True)
def scope_function():
    print("            setup before function")
    yield
    print("            teardown after function")
test_code.py
def test_always_succeeds():
    print("                test_always_succeeds")
    assert True


def test_always_fails():
    print("                test_always_fails")
    assert False


class TestFaafaaClass():
    def test_always_succeeds_under_class(self):
        print("                test_always_succeeds_under_class")
        assert True

    def test_always_fails_under_class(self):
        print("                test_always_fails_under_class")
        assert False

これをpytest -s test/sampleのように華麗に実行すると次のような結果(一部加工)が得られます。

setup before session
    setup before module
        setup before class
            setup before function
                test_always_succeeds
            teardown after function
        teardown after class
        setup before class
            setup before function
                test_always_fails
            teardown after function
        teardown after class
        setup before class
            setup before function
                test_always_succeeds_under_class
            teardown after function
            setup before function
                test_always_fails_under_class
            teardown after function
        teardown after class
    teardown after module
teardown after session

実に素晴らしい。さて、何が起こったのでしょうか。見ていきましょう。
要点は以下の2点です。
1. スコープ
2. yield

1. スコープ

前後処理を記述したい時、pytestでは@pytest.fixture()というマーキング(Javaっとした世界で言うところのアノテーション)を行います。
その際にパラメータを指定することで、実行条件を変えることができ、がその中の一つにスコープがあります。
パラメータ部分を見てもらうと scope='function'なんていう記述がありますね。それです。
このパラメータによって、セットアップコードがどの範囲で実行されるか決まります。

実行結果を見てもらうと、session > module > class > function の順でスコープが広くなることがわかりますね(指定できるのはこの4つです)。
sessionとmoduleは、1テストモジュールを使う限りは同じにスコープになっちゃいますが、
1ディレクトリに複数のテストモジュールを突っ込むとその効果をようやく噛み締めることができます。

ちなみに、@pytest.fixture()のautouseというパラメータは
pytestに気を利かせて、よしなにセットアップコードを自動実行させるためのものです。
特定のscopeを設定する場合は通常Trueにしておくと良いでしょう。後段で少し触れます。

2. yield

@pytest.fixture() マーキングするだけだと、testのメソッドよりも先に、
マーキングされたメソッドが呼び出されて終わるだけです。
Javaっとした世界的に言うところの、beforeClassやbeforeが実行されるだけです。
そこで、yieldと書いておくとよいでしょう。そうすれば、yield以降の処理は、
ここのテストケースの実行が終わった後に実行してくれるようになります。
ちなみに重箱突くと、yieldは2回以上書くと怒られます。

setup処理からparameterを引き渡したい

それはとてもシンプルです。yieldに引数をつけると良いでしょう。次のように。

conftest.py
import pytest


@pytest.fixture(scope='module', autouse=True)
def scope_module():
    print("setup before module")
    yield("    I am module fixture!")
    print("teardown after module")


@pytest.fixture(scope='class', autouse=True)
def scope_class(scope_module):
    print("    setup before class")
    print(scope_module)
    yield "        I am class fixture!"
    print("    teardown after class")


@pytest.fixture(scope='function', autouse=True)
def scope_function(scope_class):
    print("        setup before function")
    print(scope_class)
    yield("            I am function fixture!")
    print("        teardown after function")
test_code.py
def test_always_succeeds(scope_class):
    print(scope_class)
    print("            test_always_succeeds")
    assert True


def test_always_fails():
    print("            test_always_fails")
    assert False


class TestFaafaaClass():
    def test_always_succeeds_under_class(self, scope_module):
        print(scope_module)
        print("            test_always_succeeds_under_class")
        assert True

    def test_always_fails_under_class(self, scope_function):
        print(scope_function)
        print("            test_always_fails_under_class")
        assert False

各fixtureメソッドでは上位スコープのfixtureメソッドからパラメータを受け取ってprintしています。
この時のパラメータ名は、パラメータを受け取りたい先のメソッド名になります。
テストメソッドでは各fixtureで渡されたパラメータを受け取ってprintしています。
さて、次は華麗なる実行結果です。

setup before module
    setup before class
    I am module fixture!
        setup before function
        I am class fixture! 
        I am class fixture!               # test_always_succeedsメソッドでのprint
            test_always_succeeds
        teardown after function
    teardown after class
    setup before class
    I am module fixture!
        setup before function
        I am class fixture!
            test_always_fails
        teardown after function
    teardown after class
    setup before class
    I am module fixture!
        setup before function
        I am class fixture!
    I am module fixture!                  # test_always_succeeds_under_classでのprint
            test_always_succeeds_under_class
        teardown after function
        setup before function
        I am class fixture!
            I am function fixture!        # test_always_fails_under_classでのprint
            test_always_fails_under_class
        teardown after function
    teardown after class
teardown after module

実に素晴らしい。それはつまり、パラメータの受け渡しができるばかりではなく、
この例の限りにおいて、任意のfixtureからパラメータを受け取ることができたということです。
今回引き渡したパラメータはただの文字列ですが、複数のパラメータを引き渡ししたい場合等は辞書型を扱うと良いでしょう。

なお、受け取ったパラメータを上書きしても、上位スコープに影響が出ることはないようです。
あまりユースケースとしては少ないかもしれないですが、
テストケース間でglobalに(Updateされる)変数を扱いたい場合は、素直にglobal なディレクティブの変数を使うことにしています(他に良い方法があると思っていますが未調査)。

テストケースごとに固有の前後処理をしたい

はい、可能です。ではさっそく。

conftest.py
import pytest


@pytest.fixture(scope='function', autouse=True)
def scope_function():
    print("setup before function")
    yield
    print("teardown after function")


@pytest.fixture()
def fixture_a():
    print("    setup before fixture_a")
    yield("        I am fixture_a!")
    print("    teardown after fixture_a")


@pytest.fixture()
def fixture_b():
    print("    setup before fixture_b")
    yield("        I am fixture_b!")
    print("    teardown after fixture_b")


@pytest.fixture()
def fixture_c(fixture_b):
    print("        setup before fixture_c")
    print(fixture_b)
    yield("            I am fixture_c!")
    print("        teardown after fixture_c")
test_code.py
import pytest


def test_always_succeeds(fixture_a, fixture_b):
    print(fixture_a)
    print(fixture_b)
    print("            test_always_succeeds")
    assert True


def test_always_fails(fixture_b, fixture_a):
    print(fixture_b)
    print(fixture_a)
    print("            test_always_fails")
    assert False

class TestFaafaaClass():
    def test_always_succeeds_under_class(self, fixture_c):
        print(fixture_c)
        print("            test_always_succeeds_under_class")
        assert True

    def test_always_fails_under_class(self, fixture_c, fixture_b):
        print(fixture_c)
        print(fixture_b)
        print("            test_always_fails_under_class")
        assert False

説明の簡単化のため、conftest.py からfunction以外のscopeを持つfixtureメソッドを消しました。
それでは続けて、華麗なる実行結果を見てみましょう。

setup before function
    setup before fixture_a
    setup before fixture_b
        I am fixture_a!
        I am fixture_b!
            test_always_succeeds # fixture_a -> fixture_bの順で実行
    teardown after fixture_b
    teardown after fixture_a
teardown after function
setup before function
    setup before fixture_b
    setup before fixture_a
        I am fixture_b!
        I am fixture_a!
            test_always_fails    # fixture_b -> fixture_aの順で実行
    teardown after fixture_a
    teardown after fixture_b
teardown after function
setup before function
    setup before fixture_b
        setup before fixture_c
        I am fixture_b!
            I am fixture_c!
            test_always_succeeds_under_class # fixture_b -> fixture_cの順で実行
        teardown after fixture_c
    teardown after fixture_b
teardown after function
setup before function
    setup before fixture_b
        setup before fixture_c
        I am fixture_b!
            I am fixture_c!
        I am fixture_b!
            test_always_fails_under_class    # fixture_b -> fixture_cの順で実行
        teardown after fixture_c
    teardown after fixture_b
teardown after function

実に素晴らしい。個別の前後処理ができるだけじゃない。要点としては次の3点です。
1. テストメソッドの引数に指定したパラメータ順にfixtureメソッドが実行される
2. fixtureメソッド間でも依存関係を表現出来る
3. 下位層で指定された依存関係よりも上位層で指定された依存関係が優先される

まとめ

お疲れ様でした。
yieldは単にごっちゃごっちゃしたテストケースを綺麗にしてくれるだけでなく、
テストケース中にassertがfalse担った場合でも、yield以降の命令を必ず実行してくれる
という実に素晴らしい仕組みになっています。どんどん使っていきましょう。

さて、こんな便利なyieldでしたが、

もしセットアップコード(yield命令よりも前)で例外が発生した場合、
ティアダウンコード(yield命令よりも後)の部分は実行されない

ことはご存知でしょうか?

なんということでしょうか。
yieldは用法・用量を守って正しくお使いください、ということでしょうか。
ということで、次回は(気が向いたら)addfinailzerを紹介したいと思います。

参考

http://okamuuu.hatenablog.com/entry/2015/06/15/114757
http://note.crohaco.net/2016/python-pytest/
http://pythontesting.net/framework/pytest/pytest-session-scoped-fixtures/
https://docs.pytest.org/en/latest/fixture.html
http://qiita.com/koreyou/items/c7f756517c974a1a3f02
http://qiita.com/roothybrid7/items/9717137fbec2bedfd81d

90
79
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
90
79