📖

pytest 最䜎限入門

に公開
2023/08/04
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.getをmocker.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が抜けおいる)

1
onigiri.w2onigiri.w2

ご指摘ありがずうございたす。修正しおおきたした。

1