* pytest入門 [#e9ffd5bd]
#setlinebreak(on);

#contents
-- 関連
--- [[Python]]
--- [[Python覚え書き]]
--- [[Pythonのインストール]]
--- [[Python のパッケージング]]
--- [[PythonでAWS DynamoDBのCRUDを書いてみる]]
--- [[Pythonのstrptimeが遅い]]
--- [[Pythonのチューニング]]
--- [[Pythonのパフォーマンス確認]]
--- [[FizzBuzzで頭の体操]]
--- [[pytest入門]]
--- [[OpenSSLで電子署名の生成と署名検証]]
--- [[pythonでWebサーバを書いてみる]]

#html(<style>.lh17 * { line-height: 1.7}</style>)

** インストール [#q4177cd9]
#html(<div style="padding-left:10px;">)
#myterm2(){{
pip install pytest
pip install pytest-cov
}}
#html(</div>)

** ファイル/フォルダ構成 [#f0395e6c]
#html(<div style="padding-left:10px;">)
https://docs.pytest.org/en/latest/goodpractices.html#choosing-a-test-layout-import-rules

pytestのドキュメントにはいくつかの構成が記載されているが src と tests とフォルダ分ける方のが良さげ。
#html(<div style="padding:0 40px 0 10px; background:#efefef;border: 1px solid #333;">)
setup.py
src/
 mypkg/
  __init__.py
  app.py
  view.py
tests/
 __init__.py
 foo/
  __init__.py
  test_view.py
 bar/
  __init__.py
  test_view.py
#html(</div>)

*** src フォルダのPATHの通し方 [#y2046859]
#html(<div style="padding-left:10px;">)

src と tests を分ける場合で src フォルダに PATH を通すには、環境変数 PYTHONPATH を利用するか、テストコード全体の前処理(conftest.py) で sys.path に追加する。
※ テストコード自体の検索PATH は pytest.ini または pytest コマンドオプションの testpaths で指定する。

- 環境変数で行う場合
#myterm2(){{
export PYTHONPATH=./src
}}

- テストコードの前処理で行う場合
tests/conftest.py
#mycode2(){{
import sys 
import os

sys.path.append(os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/../src/"))
}}
※ conftest.py については後述。

#html(</div>)

*** __init__.py の配置についての注意点 [#xd11be7f]
#html(<div style="padding-left:20px;">)

・ src 直下には __init__.py は置かない。
・ tests 直下には __init__.py を置く。

#html(<div class="lh17" style="padding:0 40px 0 10px; background:#efefef;border: 1px solid #333;display:inline-block;vertical-align:top;">)
src/
 &color(red){%%__init__.py%%};    ...    src直下には置かない
 mypkg/
  myfunc.py
tests/
 &color(blue){__init__.py};    ...    tests直下には置く
 mypkg
  test_myfunc.py
#html(</div>)
tests 配下のパッケージ/モジュール名のフルPATHが src 配下と全く同じになってしまうと、src 側のパッケージ配下のモジュールがうまく認識されない模様。( ImportError になる )
なので、tests 配下に __init__.py をおいて全てのテストコードのパッケージの頭には必ず tests. が付くようにする。
※よく見るとドキュメントでも tests ディレクトリ直下には __init__.py が置かれている。
https://docs.pytest.org/en/latest/goodpractices.html#choosing-a-test-layout-import-rules

イメージ)
|src配下|mypkg.myfunc|
|tests配下|tests.mypkg.myfunc|

#html(</div>)


*** セットアップコードと conftest.py [#q5d6c439]
#html(<div style="padding-left:20px;">)

テストコードの前処理、後処理を行う仕組みとして fixture が用意されているが、fixture は conftest.py として別ファイルに切り出す事ができる。
また、conftest.py は各フォルダ毎に配置する事ができるので、fixture の scope=session 等と組み合わせると、各フォルダ単位で行う前後処理を記述する事ができる。
※フォルダ単位で用意する共通のテストデータの投入などはここで行うと効率的。

#html(<div class="lh17" style="padding:0 40px 0 10px; background:#efefef;border: 1px solid #333;display:inline-block;vertical-align:top;">)
tests/
 __init__.py
 &color(red){conftest.py};                  ...    テスト全体にかかるセットアップコード
 mypkg1
  __init__.py
  &color(red){conftest.py};              ...    mypkg1 配下のテスト用のセットアップコード
  test_myfunc1.py
  test_myfunc2.py
 mypkg2
  __init__.py
  &color(red){conftest.py};              ...    mypkg2 配下のテスト用のセットアップコード
  test_myfunc3.py
  test_myfunc4.py
#html(</div>)

#html(</div>)

#html(</div>)
// ファイル/フォルダ構成

** テストコードの基本形 [#kcb84353]
#html(<div style="padding-left:10px;">)

以下の命名規則に従っていれば、テストコードとして認識される。
- ファイル名のプレフィックスに test_ または サフィックスに  _test を付与する。
- テストメソッドのプレフィックスに test_ または サフィックスに  _test を付与する。

例)
#mycode2(){{
def test_sample_function1():
    print("test_sample_function1")
    assert True

def test_sample_function2():
    print("test_sample_function2")
    assert False

class TestSampleClass():
    def test_class_function():
        print("test_class_function1")               
        assert True
}}
#html(</div>)
// テストコードの基本形

** assert の書き方 [#x11919d2]
#html(<div style="padding-left:10px;">)

https://docs.pytest.org/en/latest/assert.html

*** 基本形 [#t6cbd22c]
#html(<div style="padding-left:10px;">)
#mycode2(){{
assert 真偽値(※)  [, エラーメッセージ]
}}
※ True の時に成功となる。
#html(</div>)

*** 文字列マッチング [#w2ac605a]
#html(<div style="padding-left:10px;">)
#mycode2(){{
assert 'match string' in str(result)
}}
#html(</div>)

*** 例外を期待する場合 [#v34ef498]
#html(<div style="padding-left:10px;">)
#mycode2(){{
with pytest.raises(ZeroDivisionError):
    1 / 0
}}
&br;
#mycode2(){{
with pytest.raises(ValueError, match=r'.* 123 .*'):
    myfunc()
}}
#html(</div>)

#html(</div>)
// assert の書き方

** fixtureの利用(テスト前後に行う処理) [#v69ba4d3]
#html(<div style="padding-left:10px;">)

fixtureを使用してテストの前後に行う処理を指定する事ができるので、これを利用してテストデータの投入などの行うのが良い。
※conftest.py をうまく利用すれば、パッケージ単位で共通して利用するテストデータ等も用意できるので無駄が省ける。

*** scope について [#q71d9faf]
#html(<div style="padding-left:10px;">)

#html(<div style="display:inline-block;vertical-align:top">)
fixture に scope を指定する事で、どの単位の前処理、後処理なのか明示する事ができる。

|scope|説明|h
|session|テスト全体の前後処理|
|function|各テストfunction毎に行う前後処理|
|class|各テストclass毎に行う前後処理|
|module|各テストモジュール毎に行う前後処理|
|package|各テストパッケージ毎に行う前後処理|
#html(</div>)
#html(<div style="display:inline-block;vertical-align:top;padding-left:20px;">)
例)
#mycode2(){{
import pytest

@pytest.fixture(scope='session', autouse=True)
def session_fixture():
    print("テスト全体の前処理")
    yield
    print("テスト全体の後処理")

@pytest.fixture(scope='module', autouse=True)
def module_fixture():
    print("モジュールの前処理")
    yield
    print("モジュールの後処理")


@pytest.fixture(scope='class', autouse=True)
def class_fixture():
    print("クラスの前処理")
    yield
    print("クラスの後処理")


@pytest.fixture(scope='function', autouse=True)
def function_fixture():
    print("関数の前処理")
    yield
    print("関数の後処理")
}}
#html(</div>)

#html(</div>)
// scope について end

*** yield について [#x1f624c2]
#html(<div style="padding-left:10px;">)

fixture 内に yield を記述する事で、テスト後に行う処理を記述する事が出来る。
yield が記述されていないと、全てテストの前に実行されてしまうので注意が必要。
#mycode2(){{
@pytest.fixture(scope='function', autouse=True)
def module_fixture():
    print("関数の前処理")
    yield
    print("関数の後処理")
}}
#html(</div>)

*** fixture からパラメータを受け取る [#ta69f232]
#html(<div style="padding-left:10px;">)
yield に引数を付けて実行すると、テストコード側で yield の引数を受け取る事ができる。
セットアップコードで生成したインスタンス等を利用してテストコードを実行したい場合等に利用できる。

#mycode2(){{
import pytest
import datetime

@pytest.fixture(scope='module', autouse=True)
def module_fixture1():
    print("モジュールの前処理")
    now = datetime.datetime.now()
    yield {'now': now}
    print("モジュールの後処理")

def test_fixture_param(module_fixture1):  # 引数に対象のfixtureの関数名を指定する
    now = module_fixture1['now']  # fixture側で生成したインスタンスを受け取る
    print(f'now: {now}')
}}

#html(</div>)

*** 実行する fixture をテストコード毎に指定する [#l55b07d9]
#html(<div style="padding-left:10px;">)

以下の例では、test_fixture_sample2 の時のみ、前処理、後処理として fixture_for_sample2 が実行される。
#mycode2(){{
import pytest

@pytest.fixture
def fixture_for_sample2():
    print("サンプル2用の前処理")
    yield
    print("サンプル2用の後処理")

def test_fixture_sample1():
    print('test_fixture_sample1')

def test_fixture_sample2(fixture_for_sample2):  # 引数に実行するfixtureを指定する
    print('test_fixture_sample2')

def test_fixture_sample3():
    print('test_fixture_sample3')
}}

#html(</div>)


#html(</div>)
// テストの前後に行う処理

** mark の利用 [#b678c566]
#html(<div style="padding-left:10px;">)

*** 同じテストを値を変えて行う [#gfdb5035]
#html(<div style="padding-left:10px;">)

pytest.mark.parametrize を使用すると、同じメソッドのテストを引数を変えてテストする事ができる。

#html(<div style="display:inline-block;">)
pytest.mark.parametrize を使用しない場合

#mycode2(){{
import pytest

# テスト対象の関数
def sample_div(a, b): 
    return a / b 

def test_sample_div():
    assert sample_div(10, 5) == 2
    assert sample_div(1, 1) == 1
    assert sample_div(10, 1) == 10
    with pytest.raises(ZeroDivisionError):
        sample_div(a, b)
}}
#html(</div>)
#html(<div style="display:inline-block;padding-left:20px;">)
pytest.mark.parametrize を使用する場合

#mycode2(){{
import pytest

# テスト対象の関数
def sample_div(a, b): 
    return a / b 

@pytest.mark.parametrize('a, b, expected', [
    (10, 5, 2), 
    (1, 1, 1), 
    (10, 1, 10),
    (10, 0, ZeroDivisionError),
])

def test_sample_div(a, b, expected):
    if type(expected) == int:
        assert sample_div(a, b) == expected
    else:
        with pytest.raises(expected):
            sample_div(a, b)
}}
#html(</div>)


#html(</div>)

*** テストをグループ化する [#sc0ef4e4]
#html(<div style="padding-left:10px;">)

pytest.mark に続いて任意の名前を付けておくと、テストコードをグループ化する事ができ、グループ毎にテストを実行する事ができる。
#mycode2(){{
import pytest

@pytest.mark.service1
def test_sample_func1():
    print('test_sample_func1')

@pytest.mark.service1
def test_sample_func2():
    print('test_sample_func2')

@pytest.mark.service2
def test_sample_func3():
    print('test_sample_func3')
}}

実行例 ( service1 のテストコードのみ実行する )
#myterm2(){{
pytest -m service1 -v
}}

#html(</div>)

*** 使用するfixtureをデコレータで指定する [#cdffa976]
#html(<div style="padding-left:10px;">)

メソッド引数にfixture名を指定するのではなく、@pytest.mark.usefixtures を利用する方法もある。
fixture からパラメータを受け取る必要がない場合は、こちらを使用しても良い。

#mycode2(){{
import pytest

@pytest.fixture(scope="module")
def sample_usefixtures():

    print('sample_usefixtures .. 前処理')
    yield
    print('sample_usefixtures .. 後処理')

@pytest.mark.usefixtures('sample_usefixtures')
def test_usefixtures():

    print('test_usefixtures!')
}}

pytest のドキュメントには unittest.TestCase でテストクラスのインスタンス変数を初期化する方法が記載されており、こっちが主な使い方になるのかも。
https://docs.pytest.org/en/latest/unittest.html

#html(</div>)


#html(</div>)
// mark の利用 end

** クラスやモジュールの動作を動的に変更する [#h4d0f46b]
#html(<div style="padding-left:10px;">)

*** monkeypatchを使用する [#vc99b6b2]
*** monkeypatchを使用する [#kc1cba3c]
#html(<div style="padding-left:10px;">)
#TODO

テスト対象モジュール
package1/module1.py
#mycode2(){{
def sum_all(*args):
    # result = sum(args)
    result = 0 
    for i in args:
        result += i
    return result
}}

テストコード
#mycode2(){{
import pytest
from package1 import module1

# 適当な値を返すモック
def dummy_sum_all(*args):
    return 123

def test_sum_all(monkeypatch):

    print(monkeypatch)

    # 内容を差し替え  ( monkeypatch.setattr で対象メソッドを差し替える )
    #monkeypatch.setattr(module1, 'sum_all', (lambda *x: 123))  # lambda式で書く場合
    monkeypatch.setattr(module1, 'sum_all', dummy_sum_all)

    # テスト対象のコードを実行
    result = module1.sum_all(1, 2, 3, 4, 5)

    # 期待値との比較
    expected = 15
    assert expected == result, f'期待値と異なります。期待値: {expected}, 結果: {result}'  # 123を返却するように差し替えているのでエラーになる
}}

※monkeypatch で使用できるメソッド
 https://docs.pytest.org/en/3.0.0/monkeypatch.html#method-reference-of-the-monkeypatch-fixture


#html(</div>)

#html(</div>)

** 例外を発生させたい場合 [#o4d84459]
#html(<div style="padding-left:10px;">)

*** unittest.mock を使用する [#zf1870d6]
#html(<div style="padding-left:10px;">)

前述のmonkeypatchを使用しても可能だが、ここでは unittest.mock を使用する例を挙げる。
https://docs.python.jp/3/library/unittest.mock.html

例えば、以下のコードで hello メソッド内で例外を発生させたい場合、

mymodule.py
#mycode2(){{
class MyClass1(object):

    def __init__(self):
        self.pre_message = 'Hello'

    def hello(self, name):
        return MyStringUtil.concat(self.pre_message, name)

class MyStringUtil(object):

    @classmethod
    def concat(cls, *args, suffix='!'):
        return ' '.join(args) + suffix
}}


クラスメソッドで例外を発生させたい場合は、@patch デコレータを使用
※以下は、MyClass1.hello から呼び出している MyStringUtil.concat メソッドで例外を発生させている。

test_mymodule.py
#mycode2(){{
import mymodule
from unittest.mock import patch
from unittest.mock import Mock
# from unittest.mock import MagicMock  # 呼び出し回数などを検証したい場合に使用する

@patch("mymodule.MyStringUtil.concat", Mock(side_effect=ValueError('Dummy Error!!')))
def test_hello():
    with pytest.raises(ValueError):
        mymodule.MyClass1().hello('Taro')
}}

インスタンスメソッドで例外を発生させたい場合は @patch.object を使用

test_mymodule.py
#mycode2(){{
import mymodule
from unittest.mock import patch
from unittest.mock import Mock

@patch.object(mymodule.MyClass1, 'hello', Mock(side_effect=ValueError('Dummy Error!!')))
def test_hello():
    with pytest.raises(ValueError):
        mymodule.MyClass1().hello('Taro')
}}

with 構文を使用してもOK
#mycode2(){{
from unittest.mock import patch
from unittest.mock import Mock

def test_hello():
    with patch("mymodule.MyStringUtil.concat", Mock(side_effect=ValueError('Dummy Error!!'))) as my_mock:
        with pytest.raises(ValueError):
            mymodule.MyClass1().hello('Taro')
}}

#html(</div>)
// unittest.mock を使用する

#html(</div>)
// 例外を発生させたい場合

** Tips [#me3d48f1]
#html(<div style="padding-left:10px;">)

*** 標準出力に表示する [#o9a2ab13]
#html(<div style="padding-left:10px;">)
#myterm2(){{
pytest -s
}}
#html(</div>)

*** 処理時間を計測する [#ic092ee4]
#html(<div style="padding-left:10px;">)
#myterm2(){{
pytest --durations=0
pytest -v --duration=0
}}
#html(</div>)

*** テストケース別に結果を表示する [#s0422d2a]
#html(<div style="padding-left:10px;">)
#myterm2(){{
pytest -v
}}
#html(</div>)

*** 前回NGだったケースだけテストする [#l20f53f6]
#html(<div style="padding-left:10px;">)
#myterm2(){{
pytest -v --lf
}}
#html(</div>)

*** 前回NGだったケースからテストする [#bfed747f]
#html(<div style="padding-left:10px;">)
#myterm2(){{
pytest -v --ff
}}
#html(</div>)

*** 遅いテストケースを見つける [#q5255543]
#html(<div style="padding-left:10px;">)
#myterm2(){{
pytest -v --duration=5
}}
#html(</div>)

*** テストのログをファイルに出力する [#sf30545f]
#html(<div style="padding-left:10px;">)
#myterm2(){{
pytest -v --result-log=tests/log.txt
}}
#html(</div>)

*** カバレッジを確認する [#ad801cfa]
#html(<div style="padding-left:10px;">)
#myterm2(){{
pytest -v --cov=ディレクトリ
}}
#html(</div>)

*** HTML形式のカバレッジレポートを出力する [#g6612da7]
#html(<div style="padding-left:10px;">)
#myterm2(){{
pytest -v --cov=ディレクトリ --cov-report=html
}}
#html(</div>)

*** テストで実行されなかった行を表示する [#vbe41a0d]
#html(<div style="padding-left:10px;">)
#myterm2(){{
pytest -v --cov=ディレクトリ --cov-report=term-missing
}}
#html(</div>)

#html(</div>)
// tips end

トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS