📖

pytest 最低限入門

2022/05/31に公開
2

pytestの使い方を毎回忘れてしまうので、よく使う関数や機能・ライブラリをまとめておく。
今後もまた忘れた時にこのページを見返すことにする。

基本中の基本

assert

pytestの基本中の基本。この関数で結果の検証を行う。
検証するコードがTrueを返すならテスト成功、Falseを返すならテスト失敗にする。
個人的には、これだけでテストできるシンプルさが好き。
テストライブラリによくあるassertEqualsのような検証関数が沢山あるのは苦手。

def test_ok():
    actual = 2
    assert actual == 2
    
def test_fail():
    actual = 2
    assert actual == 1

pytest.raises

これもpytestの基本中の基本。算数で言う引き算くらいの基本。
例外が発生するかのチェックを行う。
このブロックの中で例外が発生したらテスト成功、発生しなければテスト失敗。

def test_error():
    # 発生してほしいErrorクラスをraisesに引数として与える
    with pytest.raises(ValueError):
	# 例外が発生する関数実行
        func_raise_error()

絶対見逃せない便利機能:テストコードの短縮化・保守性向上

pytest.mark

基本:テストのカテゴライズ機能

@pytest.mark.databaseなどとテストケースに付与すると、カテゴリ別にテスト実行することが可能になる
以下のテストコードがある状態で、pytest -m databaseとCLIから実行すると、@mark.databaseとデコレートされてるものしか実行されなくなる。

@pytest.mark.database
    def test_database_access():
        assert ...
    @pytest.mark.no_database
    def test_print():
        assert ...

pytest.mark.parameterize

「同じようなテストするけど、データだけ違う...テストコードが太る...」ていう状況を解決してくれる。
同じテストコードに対して、似たようなデータを1つのケースでテストできる。
テストの保守性向上・可読性向上につながる。

   @pytest.mark.parametrize("data1,data2", [
   	(1, "a"),
   	(2, "b")
   ])
   def test_sample(data1, data2):
   	assert sut(data1, data2) == True

pytest.fixture

様々なテスト共通で利用する処理をまとめたり、事前処理、事後処理なんかも受け持てる。

基本中の基本

@pytest.fixtureで定義した関数の名前を、テストケースの引数に与えて利用する。
ここもassertの時と一緒で自由度が高い。
fixture関数で様々な処理をテストケースから分離できる。

@pytest.fixture
def readable_file():
    with open("test_file.txt") as f:
        return f

def test_read_file(readable_file):
    assert readable_file.read() == '...'

事後処理のやり方

答えは決まっていて、yieldを使えばいい。
テスト前はyieldまでの処理が実行され、テスト後にyield以降の処理が実行される。

@pytest.fixture
def readable_file():
    f = open("test_file.txt")
    yield f
    f.close()
    
def test_read_file(readable_file):
    assert readable_file.read() == '...'

引数を与える方法

ラップを利用して引数付きの関数を返すようにすればいい。

@pytest.fixture
def readable_file():
    def inner(path):
        with open(path) as f:
        return f
    return inner

def test_read_file(readable_file):
    assert reaable_file("test_file.txt").read() == '...'

全テスト共通の処理を定義したいなら

testsディレクトリの配下にconftest.pyを作成し、そこにfixture関数を記載すればいい。
conftest.pyにfixtureを定義すれば、様々なテストコードから利用可能になる。

# conftest.pyにreadable_fileを定義してる状態

# 同じファイルにfixture関数を定義してなくても利用可能。conftest.pyから参照してる。
def test_read_file(readable_file):
    assert reaable_file("test_file.txt").read() == '...'

引数に与えずとも、自動で実施させたい

autouse=Trueを利用すればいい。
テストケースにわざわざ引数を与えずとも、影響する全テストケースの実行前に処理が走る。

@pytest.fixture(autouse=True)
    def all_test_config():
        print('all use')

便利ライブラリ

pytest-describe:Rspec(ruby)のようにテストコードを階層構造で書ける

pip install pytest-describeでインストール可能。
describe_*という名前で関数を書けば、永遠に階層構造にできる。
describe_*配下にある、それ以外の関数はテストケースとして扱われる。

def describe_parent():
    def describe_child():
        def it_test():
            assert ...
        def describe_grandchild():
            def it_grand_test():
                assert ...
    def describe_child2():
        def it_test():
            assert ...

pytest-mock:モック ライブラリ

モックを簡単に作り出せるライブラリ。必要なら本コードの一部をモック化することも可能
mockを利用するにあたって、最低限覚えておくと便利なことだけ記載しておく

準備

1.pytest-mockをインストールする

pip install pytest-mock

2.テストケースの引数にmockerを与える(fixtureの応用)

def test_sample(mocker):
    ...

3.mockerを利用して色々モック化する

def test_sample(mocker):
    mocker.patch("src.sample.add", return_value=2)
        ...

便利機能1:mocker.Mock -- いろんな形のモックを作る --

なぜわざわざMockを使うのか?

例えば、自分でSampleMockのようなクラスを作って実装すればいいのでは?
A. 自分で作るのは面倒臭い + Mockの便利関数が利用できる

  • 流石に自分でMockを作るのは面倒臭いし、テストコードの可読性が一気に落ちる
  • また、Mockには「何回呼び出されたか?」がわかるような便利関数も用意されてる
基本①:return_value -- mockが呼出された時の返り値を設定する
>>> a = mocker.Mock(return_value='hoge')
>>> a()
'hoge'
基本②:side_effect -- mockが呼び出された時の関数そのものや例外を設定する
関数まるごと入替
>>> m = mocker.Mock(side_effect=lambda: 'hoge')
>>> m()
'hoge'
例外発生
>>> m = mocker.Mock(side_effect=Exception)
>>> m()
raise Exception(...)
基本③:return_value、side_effectの両方設定する場合、side_effectが優先される
>>> a = mocker.Mock(return_value='hoge', side_effect=lambda: 'boge')
>>> a()
'boge'
応用①:クラスを似せたオブジェクト作成
def test_sample(mocker):
    m = mocker.Mock()
    # 「m」にprop_hogeプロパティを設定(値は1)
    m.prop_hoge = 1
    # 「m」にfunc_hogeというメソッドを設定(返り値は"aaaaaa")
    m.func_boge = mocker.Mock(return_value="aaaaaa")

    # return_valueにMockを与えるってのも、利用する場面ある
    return_mock = mocker.Mock()
    return_mock.banana = "バナナ"

    # 「m」にreturn_mockを返すモック関数を設定
    m.func_return_mock = mocker.Mock(return_value=return_mock)

インスタンスのモックを作る時の基本はこれだけ
return_valueに更にモックを設定してもいいし

便利機能2:mocker.MagicMock ---- Mockのサブクラス、利用する場面は多い

Mockと機能はほとんど同じ。
Mockとの違いは、特殊メソッドをデフォルトで実装してるかどうかくらい。
参考:Python めざせモックマスター①(Mockオブジェクトのふるまいを指定する) - Qiita

便利機能3:mocker.patch ---- 本番コードの関数、クラス、モジュールをモック化できるやばいやつ

テストを実行する時だけ、本番コードの内容を変えることができる
対象がクラス、メソッド、関数、モジュールでもなんでも、モック化できる本当にやばくてcoolなやつ
基本的な入れ替え方を知っておけば、あとは大体応用が効く

関数をモックと入れ替える
sample.py
def func1(name):
    return f"hello!!{name}"
def func2(name):
    return func1(name)
test_sample.py
from sample import func2

def test_func2(mocker):
    value = "no hello"
    mocker.patch("src.sample.func1", return_value=value)
    assert func2("...") == value
クラス丸ごとモックと入れ替える
src/sample.py
def func1():
    s = Sample()
    return s.execute()

class Sample:
    def execute(self):
        return "execute"
tests/test_sample.py
from sample import Sample, func1

def test_func1(mocker):
    text = "execute2!!"
    m = mocker.Mock(spec=Sample)
    m.execute.return_value = text
    mocker.patch("src.sample.Sample", return_value=m)
    assert func1() == text
importしてるモジュールもモックと入れ替えちゃう
src/sample.py
import requests

def func1(url):
    return func2(url)

def func2(url):
    try:
        return requests.get(url).status_code
    except:
        raise Exception()
tests/test_sample.py
from src.sample import func1

def test_sample(mocker):
    status_code = 200
    url = "http://hoge.com"
    m = mocker.Mock()
    m.status_code = status_code
    mocker.patch("src.sample.requests.get", return_value=m)
    assert func1(url) == status_code

便利機能4:mocker.patch.object ---- 痒い所に手が届くmocker.patchを少し変えたやつ

mocker.patchと役割は一緒なのだが、少し使い方が異なる
「テスト対象が利用してるクラスの一部のメソッドだけモック化したい」とかいう場面で使うかも
使い方だけ見ておけば、あとはノリで行けるはず
mocker.patchと違うのは、モジュールをモック化したいなら、そのモジュールをテストファイルでimportする必要があるという点

以下はrequests.getmocker.patch.objectでモック化するパターン

src/sample.py
import requests

def func1(url):
    return func2(url)

def func2(url):
    try:
        return requests.get(url).status_code
    except:
        raise Exception()
tests/test_sample.py
import requests
from src.sample import func1

def test_sample(mocker):
    status_code = 200
    url = "http://hoge.com"
    m = mocker.Mock()
    m.status_code = status_code
    mocker.patch.object(requests, "get", return_value=m)
    assert func1(url) == status_code

参考

Discussion

quantumentanglementquantumentanglement
assert readable_file.read()

となるべきところが何カ所か

assert reaable_file.read() 

になっているところがありました。(dが抜けている)