[Python] 初中級者のためのpytest入門
2018-03-10

どうも、初中級者です。

現在自分が携わっている案件のユニットテスト近代化に伴い、そろそろ思考回路がショート寸前なので理解するために書きました。

前は公式の日本語ドキュメントがあったんですが、迷宮に迷い込んだようです(404) 。

ちなみに 英語ドキュメント は普通にあるので読める人はそっちを読んだほうがいいです。

以下のようにインストールします。

$ pip install pytest

備考

  • この記事では執筆時の最新である pytest==2.9.1 を使います。
  • pythonは 3.5.1 です。

テストランナーとしてのpytest

pytestは簡単に始められます。テストケースを置き換えなくても実行するだけでOKなこともあります。

動作を検証するために以下のファイルを作成します。

test_a.py b_test.py
# coding: utf-8
from unittest import TestCase


def test_1():
    a = 1
    b = 1
    assert a == b


def test_2():
    a = 1
    b = 2
    assert a == b


def third_test():
    raise IndexError('test')
    assert a == b


class Tekitou(TestCase):
    def test_3(self):
        a = 1
        b = 3
        self.assertEqual(a, b)


class TestMine(object):
    def test_4(self):
        a = 1
        b = 4
        assert a == b


class MyTest(object):
    def test_5(self):
        a = 1
        b = 5
        assert a == b
# coding: utf-8

def test_5():
    a = (1, 2, 3, 4, 5, 6)
    b = (1, 2, 4, 3, 5, 6)
    assert a == b


def test_6():
    a = [1, 2, 3]
    b = (1, 2, 3)
    assert a == b


def test_7():
    a = {'a': 1, 'b': 2, 'c': 3, 'd': None}
    b = {'a': 1, 'b': 4, 'c': 3}
    assert a == b


def test_8():
    a = set([1, 2, 3])
    b = frozenset([1, 2, 3])
    assert a == b

実行結果

この状態でとりあえず実行してみましょう。

(venv3.5) [vagrant@localhost pythontest]$ py.test
============================ test session starts =============================
platform linux -- Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
rootdir: /home/vagrant/pythontest, inifile:
collected 8 items

b_test.py FFF.
test_a.py .FFF

===== FAILURES =====
_____ test_5 _______

    def test_5():
        a = (1, 2, 3, 4, 5, 6)
        b = (1, 2, 4, 3, 5, 6)
>       assert a == b
E       assert (1, 2, 3, 4, 5, 6) == (1, 2, 4, 3, 5, 6)
E         At index 2 diff: 3 != 4
E         Use -v to get the full diff

b_test.py:6: AssertionError
_______ test_6 _______

    def test_6():
        a = [1, 2, 3]
        b = (1, 2, 3)
>       assert a == b
E       assert [1, 2, 3] == (1, 2, 3)
E         Use -v to get the full diff

b_test.py:12: AssertionError
_______ test_7 ______

    def test_7():
        a = {'a': 1, 'b': 2, 'c': 3, 'd': None}
        b = {'a': 1, 'b': 4, 'c': 3}
>       assert a == b
E       assert {'a': 1, 'b':... 3, 'd': None} == {'a': 1, 'b': 4, 'c': 3}
E         Omitting 2 identical items, use -v to show
E         Differing items:
E         {'b': 2} != {'b': 4}
E         Left contains more items:
E         {'d': None}
E         Use -v to get the full diff

b_test.py:18: AssertionError
______ test_2 ______

    def test_2():
        a = 1
        b = 2
>       assert a == b
E       assert 1 == 2

test_a.py:14: AssertionError
______ Tekitou.test_3 ______

self =

    def test_3(self):
        a = 1
        b = 3
>       self.assertEqual(a, b)
E       AssertionError: 1 != 3

test_a.py:26: AssertionError
______ TestMine.test_4 ______

self =

    def test_4(self):
        a = 1
        b = 4
>       assert a == b
E       assert 1 == 4

test_a.py:33: AssertionError
====== 6 failed, 2 passed in 0.12 seconds =====================

うん、わかりやすいですね。 (てかsetとfronzensetで比較結果同じなの知らなかった)

このとき「-v」を付けると差分が詳細表示されます。こんな風に..!

E         Full diff:
E         - {'a': 1, 'b': 2, 'c': 3, 'd': None}
E         ?               ^        -----------
E         + {'a': 1, 'b': 4, 'c': 3}
E         ?               ^

これがどれだけわかりやすいのかというのをみんな大好き nose と比べてみましょう。

(venv3.5) [vagrant@localhost pythontest]$ nosetests
FFF.FF.FE
======================================================================
ERROR: test_a.third_test
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/vagrant/venv3.5/lib/python3.5/site-packages/nose/case.py", line 198, in runTest
    self.test(*self.arg)
  File "/home/vagrant/pythontest/test_a.py", line 18, in third_test
    raise IndexError('test')
IndexError: test

======================================================================
FAIL: b_test.test_5
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/vagrant/venv3.5/lib/python3.5/site-packages/nose/case.py", line 198, in runTest
    self.test(*self.arg)
  File "/home/vagrant/pythontest/b_test.py", line 6, in test_5
    assert a == b
AssertionError

======================================================================
FAIL: b_test.test_6
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/vagrant/venv3.5/lib/python3.5/site-packages/nose/case.py", line 198, in runTest
    self.test(*self.arg)
  File "/home/vagrant/pythontest/b_test.py", line 12, in test_6
    assert a == b
AssertionError

======================================================================
FAIL: b_test.test_7
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/vagrant/venv3.5/lib/python3.5/site-packages/nose/case.py", line 198, in runTest
    self.test(*self.arg)
  File "/home/vagrant/pythontest/b_test.py", line 18, in test_7
    assert a == b
AssertionError

======================================================================
FAIL: test_3 (test_a.Tekitou)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/vagrant/pythontest/test_a.py", line 26, in test_3
    self.assertEqual(a, b)
AssertionError: 1 != 3

======================================================================
FAIL: test_a.TestMine.test_4
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/vagrant/venv3.5/lib/python3.5/site-packages/nose/case.py", line 198, in runTest
    self.test(*self.arg)
  File "/home/vagrant/pythontest/test_a.py", line 33, in test_4
    assert a == b
AssertionError

======================================================================
FAIL: test_a.test_2
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/vagrant/venv3.5/lib/python3.5/site-packages/nose/case.py", line 198, in runTest
    self.test(*self.arg)
  File "/home/vagrant/pythontest/test_a.py", line 14, in test_2
    assert a == b
AssertionError

----------------------------------------------------------------------
Ran 9 tests in 0.021s

FAILED (errors=1, failures=6)

こ、これは... 単純なassert文だと何が違うか表示されません。

TestCaseの assert*メソッド を使えば差異はわかりますが、これならpytestのほうが親切な気がしますよね。

テスト対象

テストケースの検出方法が異なるため、実行数もnoseとは違います。

具体的には Test~unittest.TestCase を継承したクラス か何にも属さない test_~ メソッドがテストケースと認識されます。

テスト対象を選択しないとカレントディレクトリ配下すべてがテスト対象となります。 すべてとは言っても test_~ , ~_test のファイルが対象となります。これも nose とは違いますね。

「test.py」や「tests.py」だと検出されないので注意してください。 テストファイルを引数に指定することで該当ファイルに含まれるテストケースだけに限定できます。

glob書式 を使えるので py.test test* といった書き方もできます。 また、-k を指定すると指定された文字列を含むテストケースだけが実行されます。

(venv3.5) [vagrant@localhost pythontest]$ py.test -k Mine
====== test session starts ======
platform linux -- Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
rootdir: /home/vagrant/pythontest, inifile:
collected 8 items

test_a.py F

======== FAILURES =======
________ TestMine.test_4 _______

self =

    def test_4(self):
        a = 1
        b = 4
>       assert a == b
E       assert 1 == 4

test_a.py:33: AssertionError
======= 7 tests deselected by '-kMine' ======
======= 1 failed, 7 deselected in 0.07 seconds =======

メソッドにもクラスにも該当します。

setup.cfg

setup.cfgに記述することで使うオプションの固定やテスト対象を設定できます。 または pytest.ini, tox.ini にも記述できます。

[pytest]
testpaths = .
python_files = test_*.py
python_classes = My
python_functions = test
minversion = 2.9
addopts = --maxfail=2 --showlocal --pdb

たとえば上記のようにすると次のようなルールになります。

  • カレントディレクトリの配下に存在する test*.py が対象とする
  • テストクラス名が My から始まるものを対象とする
  • テストメソッド名が test で始まるものを対象とする(これもglobが使えるっぽい)
  • pytestのバージョンが 2.9以上 であることを強制する
  • 実行時のオプションに --maxfail=2, --showlocal, --pdb を指定する(オプションは後述)

オプション

py.testは -v(差分詳細表示), -k(テストケース指定) 以外にもたくさんオプションがあります。

ただ、全部やるのはだるいので適当に選びます。

doctest-modules

doctest を実行するためのオプションです。テスト以外のモジュールであっても読み込まれて実行されます。

備考

  • doctest とは 関数(メソッド) 内に書かれた docstring に書かれたテストのことです。
  • >>> の後ろに書かれた Pythonコードを実行し、結果を 文字列 として一致比較をします。
>>> def test1():
...     """
...     it must always return 1.
...     >>> test1()
...     1
...     """
...     return 1

>>> def test2():
...     """
...     it must always return 2.
...     >>> test2()
...     2

...     but ... # doctest を終わらせるには空行が必要
...     """
...     return 3

import doctest
doctest.testmod()
**********************************************************************
File "__main__", line 4, in __main__.test2
Failed example:
    test2()
Expected:
    2
Got:
    3
**********************************************************************
1 items had failures:
   1 of   1 in __main__.test2
***Test Failed*** 1 failures.
TestResults(failed=1, attempted=2)

maxfail

一定数以上のテストケースが失敗した場合に終了となります。

(venv3.5) [vagrant@localhost pythontest]$ py.test --maxfail=1
~~ 前略 ~~
test_a.py .F

====== FAILURES ======
______ test_2 ________

    def test_2():
        a = 1
        b = 2
>       assert a == b
E       assert 1 == 2

test_a.py:14: AssertionError
!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!
== 1 failed, 1 passed, 40 pytest-warnings in 0.32 seconds ==
~~ 後略 ~~

「1」の場合は「-x」オプションと等価です。

pdb

テスト失敗時にPDBを起動します。

(venv3.5) [vagrant@localhost pythontest]$ py.test --pdb
~~ 前略 ~~
test_a.py:14: AssertionError
>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>
> /home/vagrant/pythontest/test_a.py(14)test_2()
-> assert a == b
(Pdb) p a
1
(Pdb) p b
2
(Pdb) n
F
~~ 後略 ~~

余談ですが、この機能はnoseにもあるみたいですね。しらなかった。

showlocals

テスト失敗時に変数をローカル変数を表示します。 グローバル変数と比較の結果異なったとしてもグローバル変数は表示されないので注意してください。

(venv3.5) [vagrant@localhost pythontest]$  py.test --showlocals
~~ 前略 ~~
______ test_5 ______

    def test_5():
        a = (1, 2, 3, 4, 5, 6)
        b = (1, 2, 4, 3, 5, 6)
>       assert a == b
E       assert (1, 2, 3, 4, 5, 6) == (1, 2, 4, 3, 5, 6)
E         At index 2 diff: 3 != 4
E         Use -v to get the full diff

a          = (1, 2, 3, 4, 5, 6)
b          = (1, 2, 4, 3, 5, 6)

b_test.py:6: AssertionError
~~ 後略 ~~

pastebin

恥ずかしながら私は知らなかったんですが bpaste.net というサイトに結果をアップロードしてくれます。

URLが共有できるのが有用なケースでは使えるかもしれませんね。 有効期限はちょっとわからないので使いたい人は確認してから使ってください。無責任。

(venv3.5) [vagrant@localhost pythontest]$  py.test --pastebin=failed
~~ 前略 ~~
==================== Sending information to Paste Service ====================
b_test.py:6: AssertionError --> https://bpaste.net/show/5b736ebe05c1
b_test.py:12: AssertionError --> https://bpaste.net/show/2ab1bae67924
b_test.py:18: AssertionError --> https://bpaste.net/show/45818ef23d03
test_a.py:14: AssertionError --> https://bpaste.net/show/821311a10683
test_a.py:26: AssertionError --> https://bpaste.net/show/ad8b7e245a51
test_a.py:33: AssertionError --> https://bpaste.net/show/f42e648f3e7d
====================== FAILURES ======================
~~ 後略 ~~

collect-only

テストケースだけあつめて、実行しないオプションです。

(venv3.5) [vagrant@localhost pythontest]$ py.test --collect-only
~~ 前略 ~~
<Module 'b_test.py'>
  <Function 'test_5'>
  <Function 'test_6'>
  <Function 'test_7'>
  <Function 'test_8'>
<Module 'test_a.py'>
  <Function 'test_1'>
  <Function 'test_2'>
  <UnitTestCase 'Tekitou'>
    <TestCaseFunction 'test_3'>
  <Class 'TestMine'>
    <Instance '()'>
      <Function 'test_4'>
~~ 後略 ~~

tb

(トレースバックの略らしい)テスト結果の表示フォーマットです。

long デフォルトの詳細なトレースバック形式
native Python 標準ライブラリの形式
short 短いトレースバック形式
line 失敗したテストを1行表示

durations

テストの実行時間をプロファイリングします。多い順に表示してくれます。

(venv3.5) [vagrant@localhost pythontest]$ py.test --durations=100
~~ 前略 ~~
========== slowest 100 test durations ==========
0.02s setup    b_test.py::test_5
0.00s call     b_test.py::test_5
0.00s call     b_test.py::test_7
0.00s call     test_a.py::TestMine::test_4
0.00s call     b_test.py::test_6
0.00s call     test_a.py::test_2
0.00s call     test_a.py::Tekitou::test_3
0.00s setup    test_a.py::test_1
0.00s setup    test_a.py::Tekitou::test_3
0.00s setup    test_a.py::TestMine::test_4
0.00s teardown test_a.py::TestMine::test_4
0.00s setup    b_test.py::test_6
0.00s setup    b_test.py::test_8
0.00s setup    b_test.py::test_7
0.00s setup    test_a.py::test_2
0.00s call     b_test.py::test_8
0.00s call     test_a.py::test_1
0.00s teardown b_test.py::test_5
0.00s teardown b_test.py::test_7
0.00s teardown test_a.py::test_2
0.00s teardown test_a.py::Tekitou::test_3
0.00s teardown b_test.py::test_6
0.00s teardown b_test.py::test_8
0.00s teardown test_a.py::test_1
~~ 後略 ~~

マーカー

pytestではユニットテストにマーカーを設定することでテスト呼び出しを細かく制御できます。 タグとかラベルだと思ってもらえればいいかな。

定義済みのマーカーは --markers オプションで確認できます。以下のマーカーはデフォルトで存在します。(2.9.1だと)

(venv3.5) [vagrant@localhost pythontest]$ py.test --markers
WARNING: IPython History requires SQLite, your history will not be saved
@pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value.  Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html

@pytest.mark.xfail(condition, reason=None, run=True, raises=None): mark the the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See http://pytest.org/latest/skipping.html

@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see http://pytest.org/latest/parametrize.html for more info and examples.

@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures

@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible.

@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.

skipif

skipif関数の条件を満たしたときにテストケースをスキップします。

以下(test_skipif.py)のように書くと

  • Pythonのバージョンが3.5以上の場合はtest_9がスキップ され、
  • 未満の場合はtest_10がスキップ されます。
test_skipif.py py.test test_skipif.py
# coding: utf-8
from unittest import TestCase
import pytest


@pytest.mark.skipif('sys.version_info >= (3, 5)')
def test_9():
    assert False


@pytest.mark.skipif('sys.version_info < (3, 5)')
def test_10():
    assert False
test_skipif.py sF

===== FAILURES ======
_____ test_10 _______

    @pytest.mark.skipif('sys.version_info < (3, 5)')
    def test_10():
>       assert False
E       assert False

test_skipif.py:13: AssertionError
===== 1 failed, 1 skipped in 0.05 seconds ======

条件には sys, os モジュールが使えるらしいです。(後述するxfailも同様)

xfail

テストが失敗することを期待するオプションです。

このオプションが指定されている状態で、失敗した場合は「x」、成功した場合は「X」で表示されます。

ここでいう失敗には例外も含まれますが、raises引数で例外を指定すると、その例外だけを失敗として期待します。

-rx オプションを付けることで xfailマーカー がついたテストケースの結果を表示できるのでここでは指定しましょう。

test_xfail.py py.test -rx test_xfail.py
# coding: utf-8
from unittest import TestCase
import pytest


@pytest.mark.xfail
def test_13():
    assert False


@pytest.mark.xfail
def test_14():
    assert True


@pytest.mark.xfail(reason='なんとなく')
def test_15():
    assert False


@pytest.mark.xfail('False', reason='condition=false')
def test_16():
    assert False


@pytest.mark.xfail('True', reason='condition=true')
def test_17():
    assert False


@pytest.mark.xfail(run=False, reason='run=false')
def test_18():
    assert False


@pytest.mark.xfail(run=True, reason='run=true')
def test_19():
    assert False


@pytest.mark.xfail(raises=IndexError, reason='raises=IndexError')
def test_20():
    assert False


@pytest.mark.xfail(raises=IndexError, reason='raises=IndexError')
def test_21():
    raise IndexError
test_xfail.py xXxFxxxFx
===== short test summary info =====
XFAIL test_xfail.py::test_13
XFAIL test_xfail.py::test_15
  なんとなく
XFAIL test_xfail.py::test_17
  condition=true
XFAIL test_xfail.py::test_18
  reason: [NOTRUN] run false
XFAIL test_xfail.py::test_19
  run=true
XFAIL test_xfail.py::test_21
  raises=IndexError
===== FAILURES ======
_____ test_16 _______
    @pytest.mark.xfail('False', reason='condition false')
    def test_16():
>       assert False
E       assert False
test_xfail.py:23: AssertionError
_____ test_20 _____
    @pytest.mark.xfail(raises=IndexError, reason='raises=IndexError')
    def test_20():
>       assert False
E       assert False
test_xfail.py:43: AssertionError
===== 2 failed, 6 xfailed, 1 xpassed in 0.41 seconds ======

reason引数はメモ的なもので動作には影響せず、run引数がFalseの場合はテストケースが実行されません。

parametrize

複数のパラメーターを使ったテストを簡潔に記述する方法です。 繰り返し読み込ませたいパラメータ名とパラメータを引数に指定すると、そのパラメータの数だけテストケースを実行します。

test_parametrize.py py.test test_parametrize.py
# coding: utf-8
from unittest import TestCase
import pytest


@pytest.mark.parametrize(['a', 'b', 'c'], [
    (1, 1, 1),
    (2, 2, 2),
    (3, 3, 5),
])
def test_23(a, b, c):
    assert a == b == c
test_parametrize.py ..F

====== FAILURES ======
______ test_23[3-3-5] ______

a = 3, b = 3, c = 5

    @pytest.mark.parametrize(['a', 'b', 'c'], [
        (1, 1, 1),
        (2, 2, 2),
        (3, 3, 5),
    ])
    def test_23(a, b, c):
>       assert a == b == c
E       assert 3 == 5

test_parametrize.py:12: AssertionError
======== 1 failed, 2 passed in 0.05 seconds =========

テストケースは一つですが、3回実行されていますね。

usefixtures

usefixturesはテストケース実行時に指定したfixtureを呼び出します。

フィクスチャの適用範囲や適法方法が複数存在するので、ここは少々複雑です。

fixtureは ビルトインで存在するものと自分で定義するほかに関連プラグインを入れることで使えるようになるものがあります。

自分で定義する場合は pytest.fixture デコレータを指定し、 利用する際には pytest.mark.usefixtures デコレータかテストケースメソッドの引数に指定します。

いずれも複数のfixtureを引数として指定できますが、指定したフィクスチャが存在しない場合はエラーが発生します。

ここでは標準出力の内容を見るために --capture=no をオプションとして指定します。

test_fixture.py py.test test_fixture.py --capture=no
# coding: utf-8
import pytest

@pytest.fixture()
def my_fixture1():
    print('set up 1')
    return 1


class TestMine1(object):
    def test_24(self):
        print('test 24')

    def test_25(self, my_fixture1):
        print('test 25')

    def test_26(self, my_fixture1):
        print('test 26')


# my_fixture1を呼び出したあとにmy_fixture2を呼ぶ
@pytest.fixture()
def my_fixture2(my_fixture1):
    print('set up 2')
    return my_fixture1


@pytest.fixture()
def my_fixture3():
    print('set up 3')
    return 3


class TestMine2(object):
    def test_27(self, my_fixture2):
        print('test 27')


@pytest.mark.usefixtures('my_fixture3', 'my_fixture1')
class TestMine3(object):
    def test_28(self):
        print('test 28')

    def test_29(self):
        print('test 29')


def test_30(*args, **kwargs):
    print('test30', args, kwargs)


@pytest.mark.test1
@pytest.mark.test2(1, b=2)
def test_31(request):
    print('test31', dict(request.keywords))
    def exit():
        print('exit')
    request.addfinalizer(exit)


def test_32(request2):
    print('test32', request2)


def test_33(my_fixture1, my_fixture2, my_fixture3):
    print('test33', my_fixture1, my_fixture2, my_fixture3)
~~ 前略 ~~
====== test session starts =======
platform linux -- Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
rootdir: /home/vagrant/pythontest, inifile:
plugins: ipdb-0.1.dev2
collected 10 items

test_fixture.py test 24
.set up 1
test 25
.set up 1
test 26
.set up 1
set up 2
test 27
.set up 3
set up 1
test 28
.set up 3
set up 1
test 29
.test30 () {}
.test31 {'test_31': True, 'test2': <MarkInfo 'test2' args=(1,) kwargs={'b': 2}>, 'test1': <MarkInfo 'test1' args=() kwargs={}>, 'test_fixture.py': True, 'pythontest': True}
.exit
Eset up 1
set up 2
set up 3
test33 1 1 3
.

====== ERRORS ======
______ ERROR at setup of test_32 _____
file /home/vagrant/pythontest/test_fixture.py, line 57
  def test_32(request2):
        fixture 'request2' not found
        available fixtures: cache, my_fixture2, my_fixture1, monkeypatch, pytestconfig, record_xml_property, tmpdir_factory, capsys, my_fixture3, recwarn, tmpdir, capfd
        use 'py.test --fixtures [testpath]' for help on them.

/home/vagrant/pythontest/test_fixture.py:57
===== 9 passed, 1 error in 0.05 seconds ======

フィクスチャはメソッド名により同一プロセス内で一意に識別されます。現在のスコープからオブジェクトとして参照できる必要はありません。

「usefixtures」で引数を文字列指定するのはこのためでしょう。

逆にテストケースメソッドでは引数名によりフィクスチャを特定します。(メソッド定義では引数に文字列指定できないため)

「usefixtures」デコレータはクラスに対しても指定できるため、複数のテストケースにまとめて適用したい場合に適しています。

もちろんテストケースメソッドに適用することもできます。

テストケースメソッドの引数に指定すると、フィクスチャの戻り値を受け取ることができます。 これはこれで使い道がありそうですね。

request

「request」という特殊なフィクスチャが存在します。 マーカー定義されたオブジェクトを取得したり、終了時に実行するファイナライザを定義することができます。

この辺です。

@pytest.mark.test1
@pytest.mark.test2(1, b=2)
def test_31(request):
    print('test31', dict(request.keywords))
    def exit():
        print('exit')
    request.addfinalizer(exit)

scope

これまでは使う箇所で毎回フィクスチャを指定していますが、正直めんどうですよね。

autouse オプションを有効にすることでフィクスチャの指定が不要になります。そして、どの単位で適用するかを「scope」オプションによって指定できます。

test files conftest.py py.test test_scope* –capture=no

test_scope1.py

class TestMine(object):
    def test_34(self):
        print('test 34')

    def test_35(self):
        print('test 35')

    def test_36(self):
        print('test 36')


def test_37():
    print('test 37')


def test_38():
    print('test 38')

test_scope2.py

class TestMine(object):
    def test_39(self):
        print('test 39')

    def test_40(self):
        print('test 40')
# coding: utf-8
import pytest


@pytest.fixture(autouse=True, scope='session')
def always_1():
    print('always session')


@pytest.fixture(autouse=True, scope='module')
def always_module():
    print('always module')


@pytest.fixture(autouse=True, scope='class')
def always_class():
    print('always class')


@pytest.fixture(autouse=True, scope='function')
def always_function():
    print('always function')
~~ 前略 ~~
test_scope1.py always session
always module
always class
always function
test 34
.always function
test 35
.always function
test 36
.always class
always function
test 37
.always class
always function
test 38
.
test_scope2.py always module
always class
always function
test 39
.always function
test 40
.

====== 7 passed in 0.04 seconds ======

指定できるスコープとその挙動は以下のようになります。

session テストランナー実行毎に呼び出される。
module テストモジュール毎に呼び出される。
class テストクラス毎。テストケースがクラス構造になっていない場合は都度呼び出される。
function テストケースメソッド毎に呼び出される。これがデフォルト。

conftest.py

さて、知らない子がいますね。

conftest.py です。これはpytest起動時に読み込まれる特殊なモジュールで、fixtureを記述した場合に適用されるのは配置したディレクトリ配下となります。

上の階層から参照しようとするとエラーになるので注意してください。 テストモジュールに記述すると同一モジュールのテストケースのみに適用されます。

また、このモジュール内で pytest_plugin という変数に対してリストやタプルでモジュールパスを指定することにより、自動的に読み込まれるようです。

tryfirsttrylast はフックで使うらしいけどあんまり使用する機会がなさそうだし、書くのがつらくなってきたので省略します。

気になる方はこの辺から調べてみましょう(ブーメラン)

カスタムマーカー

今までは動作に影響のあるマーカーばかりでしたが、本来マーカーは前述したようにラベルやタグのようなものです。

マーカーに意味を持たせるためにはfixtureかフックからマーカーを参照して何らかの動作をするように実装する必要がありそうです。

上記のように動作が定義されていないマーカーにも使い道はあります。以下のファイルを test_custom.py として設置してください。

test_custom.py 実行
# coding: utf-8
from unittest import TestCase
import pytest


# マーカーは複数指定できる
@pytest.mark.a
@pytest.mark.b
def test_41():
    assert False


@pytest.mark.a
def test_42():
    assert False

# クラスに適用すると配下のメソッドにも適用される
@pytest.mark.a
class TestMine(object):
    # aマーカーも適用される
    @pytest.mark.b
    def test_43(self):
        assert False

py.test test_custom.py -m "a"

====== FAILURES =====
______ test_41 ______

    @pytest.mark.a
    @pytest.mark.b
    @pytest.mark.c
    def test_41():
>       assert False
E       assert False

test_custom.py:10: AssertionError
______ test_42 ______

    @pytest.mark.a
    def test_42():
>       assert False
E       assert False

test_custom.py:15: AssertionError
______ TestMine.test_43 _______

self =

    @pytest.mark.b
    def test_43(self):
>       assert False
E       assert False

test_custom.py:22: AssertionError
======= 3 failed in 0.08 seconds ========

py.test test_custom.py -m "a and b"

======= FAILURES =======
_______ test_41 ________

    @pytest.mark.a
    @pytest.mark.b
    @pytest.mark.c
    def test_41():
>       assert False
E       assert False

test_custom.py:10: AssertionError
_______ TestMine.test_43 ________

self =

    @pytest.mark.b
    def test_43(self):
>       assert False
E       assert False

test_custom.py:22: AssertionError
======== 1 tests deselected by "-m 'a and b'" =============
======== 2 failed, 1 deselected in 0.06 seconds ===========

py.test test_custom.py -m "a and not b"

====== FAILURES ======
______ test_42 ________

    @pytest.mark.a
    def test_42():
>       assert False
E       assert False

test_custom.py:15: AssertionError
========== 2 tests deselected by "-m 'a and not b'" ========
========== 1 failed, 2 deselected in 0.05 seconds ==========

「-m」オプションを使うことで、「このマーカーのついているマーカー」、且つ、「このマーカーがついてないやつ」みたいな実行制御ができます。

マーカーは完全一致なので、「a」マーカーを指定してるのに「aa」マーカーのテストケースが取れるということはありません。

なんか、タグ検索みたいですよね?ね?

これで大まかな使い方はわかったような気がする。

次はtoxかpytestのプラギンについて書くような気がします。