LoginSignup
13
18

More than 3 years have passed since last update.

すぐに pytest 実装したい人向けのチュートリアル

Last updated at Posted at 2020-12-10

pytest について

ここでは pytest についての使用方法について紹介します。ドキュメント読んでたまに頭痛くなるので、頭痛軽減目的で書きます。

pytest とは

python でテストできるライブラリです。
pytest xxxx.py と実行することで、xxxx.py に書かれているテストケースを実行することができ、また pytest dddd/ というようにディレクトリを指定することでディレクトリ配下にある テストファイルを全て実行することができます。

なぜ pytest を使用するか?

pytest 以外のテスト系ライブラリといえば、標準搭載されている unittest があります。unittest は TestCase というクラスを継承し、それをベースにメソッドを作成することで、それぞれのメソッドに書かれているテストケースを実行することができます。
しかし、unittest だと TestCase のサブクラスを作成する必要があったり、かつ.assert のようなメソッドを使用する必要があったりして、結構面倒な実装が求められます。

それに対して、pytest は "test" という文字を関数に記載することで、それらをテストケースと認識することができ、テストを実行することができます。

unittest
from unittest import TestCase

class TryTesting(TestCase):
    def test_always_passes(self):
        self.assertTrue(True)

    def test_always_fails(self):
        self.assertTrue(False)
pytest
def test_always_passes():
    assert True

def test_always_fails():
    assert False

引用先: https://realpython.com/pytest-python-testing/

綺麗になりましたね・・・

インストール方法

pip install pytest

使い方

次に使い方について紹介します。
Lesson ベースで、以下の項目について紹介します。
これらを理解すればそれなりにいいテストコードは書けるのではと思っています。

  • 基本的な考え方 (Lesson 1)
  • テストケースの書き方 (Lesson 2)
  • スコープ (Lesson 3)
  • パラメトライズ (Lesson 4)
  • テストケース間でのデータ受け渡し (Lesson 5)

Lesson 1 - 基本的な考え方

まず、pytest の基本的な考え方について共有します。これらを抑えれば、最低限のことはできるのではと思います。

テスト変数の読み込み

最初に書いた通り、"test" という文字が関数にあれば、pytest はそれをテストケースとして認識します。

test_one.py
# lesson 1.1
a = True
def test_a():
    assert a == True
    print("yeah")

すると

>> pytest test_one.py 
============================================================================== test session starts ===============================================================================
platform darwin -- Python 3.7.3, pytest-5.3.2, py-1.9.0, pluggy-0.13.1
rootdir: /Users/xxxxxx/
collected 1 item                                                                                                                                                                 

test_one.py .                                                                                                                                                              [100%]

=============================================================================== 1 passed in 0.02s ================================================================================

みたいな結果が得られます。

テストケースで特定の変数を使用する場合、上記のようにグローバル変数を定義して、使用することも可能です。
また、以下のように関数の入力として使用することができます。ただし、テスト関数の入力変数(以降、「テスト変数」とする)として使用したい場合は必ず @pytest.fixture を書く必要があります。

test_one.py
@pytest.fixture
def variable():
    return "hi"

def test_00(variable):
    assert variable == "hi"

上記にについて解説すると、引数として variable を呼んだ場合、variable 関数が実行し、実行した結果を variable の引数としてテスト関数に渡します。複数の引数も指定することもできます。

ここで、
「え?マジで?variable という変数をどこかで定義すればできそうだけど、どうやって引数から上記の関数に結びつけるの?python ってそんなことできなくない?」
と思い、理解に苦しむ方は中にいらっしゃるのではと思います。(実際に私は初めて見たときそう思いました)

python では関数を string にしたり、引数で指定した文字等も string として読み込むことができます。
以下処理を見ればわかります。

import inspect
def iron(maiden):
    pass
print(iron.__name__) # iron
print(list(inspect.signature(iron).parameters)[0]) # maiden

関数と引数をstring なり何かしらの形でゴニョゴニョ処理しれば、対応関係を導き出すことができます。

実際は中身のコード見ないと正確に何をしているかは把握できないですが、一応コードで定義した関数と引数を使って対応関係を構築できるという認識だけ持っていただければと。(調べろよって話だけど)

pytest の実行方法

続いて、pytest の実行方法です。
pytest にいくつかオプションがあります。
以下抑えるといいでしょう。

オプション 説明
-q quite。結果は表示しない。
-v verbose。詳細なテスト結果を表示。
-s print や log 等の出力を表示。
--disable-warnings warning を消す。

その他

結果を一つのファイルに吐き出すことも可能です。

>> pytest test_one.py > result.log

以降 . が出たりすると混乱するので、これらを消す処理を pipeline に追加しました。

>> pytest test_one.py -qs | tr -d .
yeah

1 passed in 001s

Lesson 2 - テストケースの書き方

ここでは、テストケースの書き方をいくつか紹介します。

関数単位での処理

まずは基本的な書き方です。

test_two.py
@pytest.fixture
def variable():
    return "hi"

def test_00(variable):
    assert variable == "hi"

テスト変数をテスト関数の外部で定義して、テスト関数を実行しています。

クラス単位での処理

クラス単位でテスト実行することも可能です。一つのモジュール等でいろいろなテストをしたいときに、クラス単位でまとめることで可読性は上がります。

test_two.py
@pytest.fixture
def variable():
    return "hi"

class TestOne:
    def test_00(self, variable):
        assert variable == "hi"

最初にお見せした テスト変数はグローバルですが、クラスでしか使えないようにすることも可能です。

test_two.py
class TestTwo:

    @pytest.fixture
    def variable_in_c(self):
        return "hi"

    def test_01(self, variable_in_c):
        assert variable_in_c == "hi"

変数読み込みの前後処理

テスト変数をジェネレータ ( yield を使う関数) として定義することにより、変数使用前後で処理をすることも可能です。
処理前とはテストケースが始まる前を意味し、yield 前の処理が実行されます。
処理後とはテストケースが終わった後を意味し、yield 後の処理が実行されます。

test_two.py
@pytest.fixture
def variable_w_pre_post():
    print("hi-pre")
    yield "hi"
    print("hi-post")

def test_01(variable_w_pre_post):
    assert variable_w_pre_post == "hi"
    print(variable_w_pre_post)

上記のコードをファイルを test_two.py として、pytest実行すると結果は以下の通りです。

>> pytest test_two.py -sq | trim -d '.' 
hi-pre
hi
hi-post

テスト変数のimport

conftest.py というファイルでテスト変数を定義すれば、pytest.mark.usefixtures を利用することで他のファイルからアクセスすることができます。例えば、conftest.pytest_two.py というファイルが同一ディレクトリにあるとして、以下のようにそれぞれのプログラムが書かれていたとします。

conftest.py
import pytest

@pytest.fixture
def hello():
    print("hello big brother")
    yield "what have you got there?"
    print("you only see air")
test_two.py
import pytest
@pytest.mark.usefixtures("hello")
def test_hello(hello):
    print(hello)
    assert hello == "what have you got there?"

@pytest.mark.usefixtures("hello")
class TestThree:

    def test_hello(self, hello):
        print(hello)
        assert hello == "what have you got there?"

すると、以下の様な結果が得られます。

>> pytest test_two.py -sq | trim -d '.' 
hello big brother
what have you got there?
you only see air
hello big brother
what have you got there?
you only see air

Lesson 3 - スコープ

次にスコープについて紹介します。

スコープの意味

スコープを指定することにより、変数の寿命を定義することができます。fixturescope を定義することで寿命範囲を指定することができます。ここで言う寿命とは出力した変数を保持する範囲と思ってください。

例えば、以下のようなテストがあったとします。

test_three.py
import pytest

@pytest.fixture(scope="class")
def variable_c():
    print("variable_c_start")
    yield "variable_c"
    print("variable_c_end")


@pytest.fixture(scope="function")
def variable_f():
    print("variable_f_start")
    yield "variable_f"
    print("variable_f_end")

class TestC:

    def test_00(self,variable_c,variable_f):
        assert variable_c == "variable_c"
        assert variable_f == "variable_f"

    def test_01(self,variable_c,variable_f):
        assert variable_c == "variable_c"
        assert variable_f == "variable_f"

テスト変数 variable_c はクラスに突入したら yield までが実行され、クラスから抜けたら yield 以降を実行します。variable_f も然り。

これを実際に実行してみると、以下のような結果になります。

>> pytest test_three.py -sq | tr -d .
variable_c_start
variable_f_start
variable_f_end
variable_f_start
variable_f_end
variable_c_end

スコープの種類

以下のスコープがあります
- session
- module
- class
- function

スコープでの注意

test_three.py
import pytest

@pytest.fixture(scope="class")
def variable_c():
    print("variable_c_start")
    yield "variable_c"
    print("variable_c_end")


@pytest.fixture(scope="function")
def variable_f():
    print("variable_f_start")
    yield "variable_f"
    print("variable_f_end")

def test_02(variable_c,variable_f):
    assert variable_c == "variable_c"
    assert variable_f == "variable_f"

を実行すると

>> pytest test_three.py -sq | tr -d .
variable_c_start
variable_f_start
variable_f_end
variable_c_end

が出ます。「クラスのテスト指定したのになぜ関数で実行するんだ!!」と思うかもしれません。
基本的に
session >> module >> class >> function
と言う順でテスト変数が処理されます。テスト抜ける時は上記の逆です。
そのため、上記のテストにおいて scop=function で指定したテスト変数の前後処理に対して、scop=class で指定したクラス変数の前後処理の結果が外側にあるわけです。

Lesson 4 - パラメトライズ

醍醐味のパラメトライズです。私が認識している限り2種類あるかと思います。(他にあるかもしれませんが、下記で十分対応可能かと)

  • fixture params
  • pytest.mark.parametrize

以下、それぞれについて紹介します。

fixture params

テスト変数を params と言う引数を使い、それにデータの配列を与えることでそれぞれのデータをベースにテストすることができます。ただし、テスト変数にrequest と言う引数を定義する必要があります。

test_four.py
@pytest.fixture(params=[0,1])
def x2(request):
    return request.param + 1

def test_01(x2):
    assert x2 < 3
    assert x2 > 0
    print("-->",x2)

上記をテストすると、以下のようになります。

>> pytest test_four.py -sq | tr -d .
--> 1
--> 2

また、組み合わせのテストも実行できます。

test_four.py
@pytest.fixture(params=[0, 1])
def x3(request):
    return request.param

@pytest.fixture(params=[2, 3, 4])
def y3(request):
    return request.param

def test_02(x3,y3):
    assert x3*y3 > -1
    print(x3*y3,"=",x3*y3)

すると、以下のような結果となります。

>> pytest test_four.py -sq | tr -d .
0 = 0
0 = 0
0 = 0
2 = 2
3 = 3
4 = 4

さらに複数の値をセットでテストしたい場合は、params の配列の中に list 等を入れれば可能です。

test_four.py
@pytest.fixture(params=[
    [0, 1, 0],
    [0, 2, 0],
    [1, 2, 2],
    [2, 2, 4],
    ])

def arr(request):
    return request.param

def test_03(arr):
    assert arr[0] * arr[1] == arr[2]
    print(arr[0] * arr[1], "==", arr[2])

pytest.mark.parametrize

上記ではテスト変数を定義してパラメトライズしてからテストケースに使用しましたが、ここではパラメトライズした変数をダイレクトにテストケースに入力します。

test_four.py
@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3, 4])
def test_04(x,y):
    print("test_04")
    assert x*y > -1
    print(x*y)

すると以下の結果が得られます。

>> pytest test_four.py -sq | tr -d .
0
2
0
3
0
4

また複数の値をセットでテストすることもできます。

test_four.py
@pytest.mark.parametrize("x,y,z", [
    [0, 1, 0],
    [0, 2, 0],
    [1, 2, 2],
    [2, 2, 4],
    ])
def test_05(x,y,z):
    assert x*y == z
    print(x*y,"=",z)

実行結果は以下の通りです。

>> pytest test_four.py -sq | tr -d .
0 = 0
0 = 0
2 = 2
4 = 4

Lesson 5 - テストケース間でのデータ受け渡し

これまで、各テストケースで実行した際に変数の値を指定しましたが、テストケース跨いだ方法ではありませんでした。pytest でいくつかテストケース調べても直列系のテストはあまり出てこないので、できないと思われがちです。しかし、実際にテストケース跨いだ直列処理はできます。

直列系の中でもよく見かけるのはこのリンクにある pytest_namespace がありますが、私はこれで痛い思いをしたので別の方法を紹介します。

今回紹介するのは、Lesson 3で紹介した方法の応用です。
mutable object を使うと言う方法です。
例えば、クラスの中で複数のテストケースでデータを受け渡ししたい場合は、scope="class" mutable object をテスト変数として定義することで実現できます。

test_five.py
@pytest.fixture(scope="class")
def passing_var():
    print("passing_var start")
    yield ["a"]
    print("passing_var end")


class Test00:

    def test_a(self, passing_var):
        print("test_a")
        print(passing_var[0])
        assert passing_var[0] == "a"

        passing_var[0] = "b"

    def test_b(self, passing_var):
        print("test_b")
        print(passing_var[0])
        assert passing_var[0] == "b"

        passing_var[0] = "c"

    def test_c(self, passing_var):
        print("test_c")
        print(passing_var[0])
        assert passing_var[0] == "c"

すると、以下の結果が得られます。

>> pytest test_five.py -sq | tr -d .
passing_var start
test_a
a
test_b
b
test_c
c
passing_var end

また、複数のデータセットをベースにテストしたい場合は以下のやり方もあります。

test_five.py
@pytest.mark.parametrize("x", [0, 3, 6])
def x2(x):
    return x

@pytest.fixture(scope="class",params=[0,3,6])
def passing_var_2(request):
    print("passing_var start")
    yield [request.param]
    print("passing_var end")


class Test01:

    def test_1(self, passing_var_2):
        print("test_1")
        print(passing_var_2[0])
        assert passing_var_2[0] % 3 == 0

        passing_var_2[0] += 1

    def test_2(self, passing_var_2):
        print("test_2")
        print(passing_var_2[0])
        assert passing_var_2[0] % 3 == 1

        passing_var_2[0] += 1

    def test_3(self, passing_var_2):
        print("test_3")
        print(passing_var_2[0])
        assert passing_var_2[0] % 3 == 2

結果は以下の通りです。

>> pytest test_five.py -sq | tr -d .
passing_var start
test_1
0
test_2
1
test_3
2
passing_var end
passing_var start
test_1
3
test_2
4
test_3
5
passing_var end
passing_var start
test_1
6
test_2
7
test_3
8
passing_var end

最後に

いかがでしたでしょうか。今回私が知っている pytest の基本的な使い方を一通り紹介しました。おそらく、上記をしれば怖いもの無しかと思っています。 reporting の操作等もありますが、それらはそこまで重要ではないと思っているので割愛しました。

github にもサンプルコードも用意しました。もしよければご覧ください。
github: https://github.com/mikeogawa/pytest_samples

ありがとうございました。

参考文献

https://realpython.com/pytest-python-testing/
https://stackoverflow.com/questions/49238725/chaining-tests-and-passing-an-object-from-one-test-to-another

13
18
0

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
13
18