[Python] 初中級者のためのpytest入門

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

日本語ドキュメントはすでに存在するので、余裕のある方はそっち見たほうがいいかも。余裕のない人もドキュメント見た方がいいかも。

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

$ 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 = <test_a.Tekitou testMethod=test_3>
 
    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 = <test_a.TestMine object at 0x7f883c29d4a8>
 
    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 = <test_a.TestMine object at 0x7f359a3d00b8>
 
    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」を実行するためのオプションです。テスト以外のモジュールであっても読み込まれて実行されます。

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_skipif.py py.test test_skipif.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」オプションによって指定できます。

conftest.py py.test test_scope* –capture=no
# 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 ================================
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')

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

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

さて、知らない子がいますね。
「conftest.py」です。これはpytest起動時に読み込まれる特殊なモジュールで、fixtureを記述した場合に適用されるのは配置したディレクトリ配下となります。
上の階層から参照しようとするとエラーになるので注意してください。テストモジュールに記述すると同一モジュールのテストケースのみに適用されます。

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

tryfirstとtrylastはフックで使うらしいけどあんまり使用する機会がなさそうだし、書くのがつらくなってきたので省略します。
気になる方はこの辺から調べてみましょう(ブーメラン)

カスタムマーカー

今までは動作に影響のあるマーカーばかりでしたが、本来マーカーは前述したようにラベルやタグのようなものです。
マーカーに意味を持たせるためにはfixtureかフックからマーカーを参照して何らかの動作をするように実装する必要がありそうです。

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

test_custom.py py.test test_custom.py -m “a”
# 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
====================================== 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 = <test_custom.TestMine object at 0x7fcb28c49550>
 
    @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 = <test_custom.TestMine object at 0x7fd5bf128eb8>
 
    @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のプラギンについて書くような気がします。