pytest を使ってみる（基本のassert / フィクスチャ / パラメタ）【Python】

# やりたいこと
Pythonのテスト用フレームワークのひとつ「pytest」に入門する。

# Pythonのテストについて
Pythonのテスト用フレームワークとしては今回取り上げる「pytest」以外にも、標準モジュールの「unittest」や「doctest」が存在します。
ですが、最近人気なのはサードパーティ製の「pytest」のようです。
それぞれのフレームワークの使い方は[こちら](https://qiita.com/flcn-x/items/fcbbc2fb291b970290f2)の記事、pytestをおすすめする理由は[こちら](https://myenigma.hatenablog.com/entry/2021/01/31/212743)の記事に、わかりやすくまとまっていたので参照ください。


# 使ってみる
## インストール
```bash
pip install pytest
```

## 基本のテスト
- ターミナルで `pytest` を実行すると、カレントディレクトリ以下に存在するテストがすべて実行されます。
- 特定のテストファイルのみを実行したいときは `pytest tests/test_hoge.py` のようにファイル名を指定します。
- 「test」から始まるファイル・メソッドが、テストのコードであると認識されます。ですので `test_hoge.py` の中にテストしたい関数を `def test_fuga(): ~~` というように定義しましょう。
- `assert`に、検証したい条件式を記述します。ちなみに「assert」とは「断言する」「主張する」という意味らしいです。

```py:test_sample.py
# テストしたい関数
def add(a, b):
  return a + b

# テスト（成功パターン）
def test_expected():
  assert add(3, 5) == 8

# テスト（失敗パターン）
def test_unexpected():
  assert add(3, 5) == 10
```

ターミナルでテストを実行します。
```bash
$ pytest tests/test_sample.py    
```

テスト結果は以下のようになりました。

```bash                                                                                                   ✘ 1 
=================================================================== test session starts ===================================================================
platform darwin -- Python 3.8.10, pytest-7.0.1, pluggy-1.0.0
rootdir: /Users/user/Documents/hoge
plugins: cov-3.0.0
collected 2 items                                                                                                                                         

tests/test_sample.py .F                                                                                                                            [100%]

======================================================================== FAILURES =========================================================================
_____________________________________________________________________ test_unexpected _____________________________________________________________________

    def test_unexpected():
>       assert add(3, 5) == 10
E       assert 8 == 10
E        +  where 8 = add(3, 5)

tests/test_sample.py:16: AssertionError
================================================================= short test summary info =================================================================
FAILED tests/test_sample.py::test_unexpected - assert 8 == 10
=============================================================== 1 failed, 1 passed in 0.03s ===============================================================

```

テストが始まってすぐあたりの
```bash
tests/test_sample.py .F
```
という出力ですが、
今回のファイルにある２つのテストのうち、１つ目が成功（ピリオド「.」）、２つ目が失敗（Failed「F」）であることを表しています。

その後、失敗した２つ目のテストの詳細が`AssertionError`として出力されています。



## テスト前後で前処理・後処理を行う
テストの前後に処理を行いたい場合は「フィクスチャ」という機能を使うことができます。
- 「フィクスチャ」とは「テスト前後にpytestにより実行される関数」とのことでした（[参考](https://www.nooozui.com/entry/20191205/1575555918)）。（汎用的な言葉ではなく、どちらかというと専らpytestの文脈で使用される言葉なのでしょうか...？）

- 関数を `@pytest.fixture`でアノテートすることで、その関数をフィクスチャとして使用することができます。
- フィクスチャ関数における `yield` にて、テストが呼び出されます。

- テストを行う関数の引数に、フィクスチャの関数を引数として渡すことで、フィクスチャとして使用します。



以下では、テストの前後にメッセージをprintする簡単な例を見てみます。

フィクスチャとして使用する関数`decorate()`の `yield`の部分に、テストを実行する関数 `test_[un]expected()`が埋め込まれる、みたいなイメージです。


```py:test_sample.py
import pytest

# テストしたい関数
def add(a, b):
    return a + b

# フィクスチャ
@pytest.fixture()
def decorate():
    # 前処理
    print('テストを始めます！')

    # テスト実行
    yield

    # 後処理
    print('テストを終わります！')
    print('================')

# テスト（成功パターン）
def test_expected(decorate):
    print('テストやってます！')
    assert add(3, 5) == 8

# テスト（失敗パターン）
def test_unexpected(decorate):
    print('テストやってます！')
    assert add(3, 5) == 9
```

コマンドでテストを実行してみましょう。
テスト結果内にてprint()の結果を見るためには、オプション`caputure=no`を設定します。

```bash
$ pytest tests/test_sample.py --capture=no     
```

テスト結果は以下のようになりました。

```bash
============================= test session starts ==============================
platform darwin -- Python 3.8.10, pytest-7.0.1, pluggy-1.0.0
rootdir: /Users/user/Documents/hoge
plugins: cov-3.0.0
collected 2 items

tests/test_sample.py テストを始めます！
テストやってます！
.テストを終わります！
================
テストを始めます！
テストやってます！
Fテストを終わります！
================


=================================== FAILURES ===================================
_______________________________ test_unexpected ________________________________

decorate = None

    def test_unexpected(decorate):
        print('テストやってます！')
>       assert add(3, 5) == 9
E       assert 8 == 9
E        +  where 8 = add(3, 5)

tests/test_sample.py:31: AssertionError
=========================== short test summary info ============================
FAILED tests/test_sample.py::test_unexpected - assert 8 == 9
========================= 1 failed, 1 passed in 0.02s ==========================

```

以下の部分がテストの概要ですね。

```bash
tests/test_sample.py テストを始めます！
テストやってます！
.テストを終わります！
================
テストを始めます！
テストやってます！
Fテストを終わります！
================
```

１回目のテストでは成功（「.」）し、２回目のテストでは失敗（「F」）しています。
テスト前後には「テストを始めます！」「テストを終わります！」とprintされています。




## 複数のテストに共通するリソースを作成する
このような場合も、フィクスチャを活用することができます。

ここでは、1~20のランダムな整数を返す関数 `random_number()` をフィクスチャにして、複数のテスト関数にてテストします。（どんなテストやねんて感じですが、、）
１回目のテストは偶数であること、２回目のテストでは１０以上であることを成功条件にしています。


今回フィクスチャを定義する際は
```py
@pytest.fixture(scope='module')
```
というふうに記述し、フィクスチャのスコープを設定しています。
`module`に設定すると、モジュール（＝ソースコード）で一回だけフィクスチャが呼び出されることとなり、１回目のテストと２回目のテストで同じ値を渡すことができます。

スコープには以下を設定することができます。

スコープ | フィクスチャの実行タイミング
--- | ---
function | テストケース（テスト関数）ごとに実行（デフォルト）
module | テストファイルで1回実行
class | テストクラスで1回実行
session | テスト全体で1回実行


```py:test_sample.py
import pytest
import random


# フィクスチャ
@pytest.fixture()
def random_number():
    return random.randint(1, 20)  # 1~20のランダムな整数を返す


# テスト（random_number()の戻り値が偶数の場合、成功）
def test1(random_number):
    print('\n')
    print('random_number: {}'.format(random_number))
    assert random_number % 2 == 0


# テスト（random_number()の戻り値が10以上の場合、成功）
def test2(random_number):
    print('\n')
    print('random_number: {}'.format(random_number))
    assert random_number >= 10
```

コマンドでテストを実行してみましょう。
テスト結果内にてprint()の結果を見るためには、オプション`caputure=no`を設定します。

```bash
$ pytest tests/test_sample.py --capture=no     
```

テスト結果は以下のようになりました。

```bash
✘ 1 
=================================================================== test session starts ===================================================================
platform darwin -- Python 3.8.10, pytest-7.0.1, pluggy-1.0.0
rootdir: /Users/user/Documents/hoge
plugins: cov-3.0.0
collected 2 items                                                                                                                                         

tests/test_sample.py 

random_number: 13
F

random_number: 13
.

======================================================================== FAILURES =========================================================================
__________________________________________________________________________ test1 __________________________________________________________________________

random_number = 13

    def test1(random_number):
        print('\n')
        print('random_number: {}'.format(random_number))
>       assert random_number % 2 == 0
E       assert (13 % 2) == 0

tests/test_sample.py:15: AssertionError
================================================================= short test summary info =================================================================
FAILED tests/test_sample.py::test1 - assert (13 % 2) == 0
=============================================================== 1 failed, 1 passed in 0.03s ===============================================================


```

以下の部分が２回のテストの概要です。

```
random_number: 13
F

random_number: 13
.
```
今回フィクスチャのスコープを「module」（テストファイル）に設定したため、１回目も２回目も同じ13という値が渡っています。
偶数でないため1回目は失敗（「F]）、10以上のため2回目は成功（「.」）しています。



## 複数パターンでテストする
ひとつのテスト関数で、複数パターンのテストをしたい場合は、引数を渡してあげます。
引数を渡すときは
```py
@pytest.mark.parametrize('引数の名前', [引数の中身（複数）])
```
というデコレータを使えばできます。

以下では、aとbという引数にそれぞれ「2と3を渡すテスト」「1と7を渡すテスト」を行う例を行います。2つの引数の和が偶数であれば、成功としています。

```py:test_sample.py
import pytest

@pytest.mark.parametrize('a, b', [(2, 3), (1, 7)])
def test_add(a, b):
    result = a + b
    assert result % 2 == 0
```

`pytest tests/test_sample.py`でテストを実行した結果は以下のようになりました。

```bash
============================= test session starts ==============================
platform darwin -- Python 3.8.10, pytest-7.0.1, pluggy-1.0.0
rootdir: /Users/user/Documents/hoge
plugins: cov-3.0.0
collected 2 items

tests/test_sample.py F.                                                 [100%]

=================================== FAILURES ===================================
________________________________ test_add[2-3] _________________________________

a = 2, b = 3

    @pytest.mark.parametrize('a, b', [(2, 3), (1, 7)])
    def test_add(a, b):
        result = a + b
>       assert result % 2 == 0
E       assert (5 % 2) == 0

tests/test_sample.py:7: AssertionError
=========================== short test summary info ============================
FAILED tests/test_sample.py::test_add[2-3] - assert (5 % 2) == 0
========================= 1 failed, 1 passed in 0.02s ==========================

```

以下の出力から、1回目は失敗（2+3は偶数ではない）、2回目は成功（1+7は偶数である）していることがわかります。
```
tests/test_sample.py F.   
```



## まとめ
今回はpytestに入門してみました。
まだまだ他にも便利な使い方があるようなので（モックとか）、使うことがあったら記事にしてみます。


# 参考記事
- [PythonのデフォルトのUnitTestsではなくpytestを使うべき7つの理由
2021-01-31
](https://myenigma.hatenablog.com/entry/2021/01/31/212743)
- [[Python] 関数テストと手法の概要比較（doctest, unittest, pytest）](https://qiita.com/flcn-x/items/fcbbc2fb291b970290f2)
- [テスト駆動Python読んだ 〜その2:フィクスチャとは？組み込みフィクスチャを使ってみる〜
](https://www.nooozui.com/entry/20191205/1575555918)
- [pytestのfixture、基本のき](https://qiita.com/aki3061/items/53839cbbbb6073c64406)
- [ゼロから学ぶPython pytest](https://rinatz.github.io/python-book/ch08-02-pytest/)