Pytestでテストをもっと効率化してみたお話


## はじめに
本記事はpytestをもっと使えこなせるようになるために学んだ便利な機能を技術共有と自身の備忘録もかねて記載してきます。

初めて実装した時のお話はこちら→[Pytestを用いたAPIテストを初めて実装した時の話](https://qiita.com/inacyc_k/items/4eda8918bcc4699bbe53)
基本的な部分はこちらをご参考ください。

メインの参考書は以下です。（要約ページではないのでご注意を！）

https://www.amazon.co.jp/%E3%83%86%E3%82%B9%E3%83%88%E9%A7%86%E5%8B%95Python-%E7%AC%AC2%E7%89%88-Brian-Okken/dp/4798177458

追加で業務内での利用内容を含んでいるため、上記参考書通りの記載とは異なることをご了承下さい。また、テスト駆動に関するまとめではなく、あくまでもpytestの技術を学びました。
テスト駆動開発については、読みながら初めて学び興味をもったので、また別の記事にてまとめさせてください。

では早速より良いテストコードのための便利な機能についてご紹介していきます。

:::note info
各表題の前の[]には、参考書の章番号を記載しております。もっと詳しく知りたい！と思ったらぜひ合わせて読んでみてくださいね！
:::

## [3.4章]フィクスチャのスコープ

フィクスチャとは、テスト関数の前処理を行う関数のことを言います。このフィクスチャは基本テスト関数の前にそれぞれ実施をしますが、共通の処理やとても時間がかかる処理など毎回実施しなくて良いフィクスチャも存在します。そんな時に役立つ、実行順序を定義できるスコープのご紹介です。

基本、フィクスチャにはこの関数がフィクスチャですよ、とするために`@pytest.fixture()`というデコレータを付与します。

このデコレータにスコープ情報を付与して、フィクスチャ実行タイミングを変更することができます。

```python
@pytest.fixture(scope='function')
```
テスト関数やメソッドごとに1回実行されます。スコープを指定しない場合のデフォルトです。
```python
@pytest.fixture(scope='class')
```
テストクラスごとに1回実行されます。
```python
@pytest.fixture(scope='module')
```
モジュールごとに1回実行されます。
```python
@pytest.fixture(scope='package')
```
パッケージごとに1回実行されます。
```python
@pytest.fixture(scope='session')
```
セッションごとに1回実行されます。セッションとはpytestコマンドを利用してテストを1回実行される間を指します。

このように、どの単位で実行するかをフィクスチャごとに定めることができます。

しかし、次に重要になるのは実行されるフォクスチャがどこに記述されているかです。もし、テストモジュール内で定義されている場合は、`package`スコープおよび`session`スコープは`module`スコープと同じになります。他のスコープを利用したい場合は次で説明する、`conftest.py`ファイルを利用する必要があります。

## [3.5章]フィクスチャをファイルに分ける

複数のテストファイルで共有したいフィクスチャには`conftest.py`ファイルを利用します。
このファイルはテストファイルと同じディレクトリ、または親ディレクトリに配置します。

例えば以下のようなファイル構成が想定できます。
```bash
ルート  
└── tests/  # テスト関連のディレクトリ（全体で共有するフィクスチャを定義）  
    ├── __init__.py  # パッケージとして認識させるためのファイル  
    ├── conftest.py  # すべてのテストで共有される pytest のフィクスチャファイル  
    │  
    ├── unit/  # ユニットテスト用ディレクトリ 
    │   ├── __init__.py  # パッケージとして認識させるためのファイル  
    │   ├── conftest.py  # unit テスト専用の pytest のフィクスチャファイル 
    │   ├── test_1.py  # ユニットテスト  
    │   └── ...  
    │  
    └── integration/  # 統合テスト用ディレクトリ
        ├── __init__.py  # パッケージとして認識させるためのファイル  
        ├── conftest.py  # integration テスト専用の pytest のフィクスチャファイル
        ├── test_A.py  # 統合テスト  
        └── ...  
```
全てのテストで利用するようなフィクスチャは親ディレクトリに、それぞれのテスト専用のフィクスチャはテストディレクトリに、という管理が可能です。

またフィクスチャがどこにあるか簡単にわかるコマンドもあります。
```python
pytest --fixtures -v
```
実行すると、、？
<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/42cf1020-36ed-16a6-ffb0-2db69109fb9f.png">

このように、どのディレクトリにフィクスチャがあるかが一覧で確認できます。docstringの記載があれば、その詳細も合わせて出力されます。


:::note info
conftest.pyファイルはpytestによって自動的に読み込まれるため、インポートは不要です。
:::

## [5.2章]テスト関数をパラメータ化

テストを実施する際、さまざまなパターンでのテストを実施します。パラメータが1のとき、0のときなどなど。同じテスト関数を冗長的に繰り返すのは、無駄に感じます。そんな時に役立つ、テスト関数のパラメータ化です。

pytestでテスト関数をパラメータ化するにはテスト関数の前に`@pytest.mark.parametrize`というデコレータを付与します。
1つ目の引数は、パラメータの名前のリストです。ここでは引数を`event`という名称でパラメータを引き渡しています。Assert内容もパラメータ化したいなど複数項目を引き渡すことも可能です。
2つ目の引数は、テストケースのリストです。実施したいテストケース分記述します。今回は各項目にテスト名を付与したかったので、`id`を加えています。

（以下、少し参考書と異なる記載方法です。）
```python
@pytest.mark.parametrize(
    ["event"],
    [
        pytest.param(
            {"type": "hoge"},
            id="pattern1"
        ),
        pytest.param(
            {"type": "huga"},
            id="pattern2"
        ),
    ]
)
def test_1(event):
    """テスト1"""
    response = lambda_handler(event, None)
    # レスポンスの検証
    assert response.get('body') == "OK"

```

実行してみると、、？

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/4fa484bd-ab88-9ace-a0de-c3e9235c468e.png">


テスト関数`test_1`に対して、`[pattern1]`と`[pattern2]`の2パターンでテストを実行することができました！
テスト関数自体は1つですが、引き渡すパラメータごとにテストを実施することができました。

参考:[pytestのmark.parametrizeでテスト名を付ける](https://dev.classmethod.jp/articles/pytest-param-id/)

## [8章]pytestの設定ファイル
pytestの設定ファイルを作成し、便利なpytestライフにしましょう。
今回は2つの設定をご紹介。

- `testpaths = tests`
コマンドラインでファイル、またはディレクトリを指定しなった場合にどこのテストを実施するかを指定するコマンドです。`testpaths = tests`とすると、`test`ディレクトリのテストが実行されます。

- `addops = 〜`
常に実行したいpytestのフラグを指定できます。必要に応じた項目を設定ください。
おすすめのフラグが以下になります。

| フラグ                          | 説明 |
|-------------------------------------|--------------------------------------------|
| `-v` / `--verbose`                 | 詳細な出力を表示する（実行中のテストを明示） |
| `-q` / `--quiet`                    | 簡潔な出力にする（テスト結果のみを表示） |
| `--maxfail=3`                       | 3 回失敗した時点でテストを停止する |
| `--disable-warnings`                | 警告を非表示にする |
| `--strict-markers`                  | 存在しないマーカーを使った場合にエラーを出す |
| `--strict-config`                   | `pytest.ini` の設定ミスがある場合にエラーを出す |
| `--capture=no`                      | `print` 出力をキャプチャしない（デバッグ向け） |
| `--cov=tests`                       | `pytest-cov` を使い `tests/` のカバレッジを計測 |
| `--durations=5`                     | 実行時間の長い **上位5つ** のテストを表示 |
| `-x` / `--exitfirst`                | 最初の失敗でテストを即停止 |
| `--ff` / `--failed-first`           | 直前に失敗したテストを優先的に実行 |
| `--lf` / `--last-failed`            | 直前に失敗したテスト **のみ** 実行 |


今回紹介した設定以外にもたくさんの設定ができるので、詳しくはこちら→[pytest公式 Configuration Options](https://docs.pytest.org/en/stable/reference/reference.html#configuration-options)

## [9章]カバレッジ

作成したテストがコードのどれくらいカバーできているのかを計測できるのがコードカバレッジです。
今回は、pytestのプラグインでカバレッジ計測の便利なライブラリである「pytest-cov」を利用します。

まずはインストールから。
```bash
pip install pytest-cov
pip install coverage（pytest-cov依存関係先のため必要に応じ）
```

実行には`--cov`フラグを追加し、計測したいコードパス（ここではコードが`domain`フォルダ配下）を指定します。

```python
pytest --cov=domain 
```
これを実行すると、、？

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/2be7e0df-c49d-9f0c-fd4c-9bf1a43cc36f.png">


テスト実行結果の後ろにカバレッジの結果が表示されています。
今回のコードは1ファイルのみであり、カバレッジは100％、つまり全ての行にテストよりアクセスできている、ということになります。
だからといって、コードに対し十分にテストされている、というわけではないので内容の精査は別途必要になります。

カバレッジをHTML出力することで、さらに詳細に確認することができます。
```python
pytest --cov=domain --cov-report=html 
```
作成されたレポートはコマンドを実行したディレクトリにhtmlcov/フォルダとして作成されます。
その中のhtmlファイルのパスをコピーして、ブラウザ表示してみると。。？

<img width="85%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/ebee44bb-4a55-5335-96f1-7fe9d5369f32.png">


先ほどの結果がwebブラウザ上で確認できます。
またコード格納されているファイル（ここではdomain/app.py）をクリックすると、1行ずつの詳細が確認できます。


## [2.6章]例外テスト
テスト対象のコードで意図的に例外を発生させる場合、通常の assert では検証できません。pytest では with pytest.raises(例外) を使用して、特定の例外が発生することをテストします。
```python
def lambda_handler(event, context):
    # 例外を発生
    raise TypeError()
```
このようなコードをテストする場合は、`with.pytest.raises(例外)`を利用します。括弧（）の中は、`TypeError`や`ValueError`、`Exception`など例外の種類が入ります。

```python
def test_1(event):
    """テスト1"""
    with pytest.raises(TypeError):
        lambda_handler({}, None)
```
今回は`TypeError`を発生させているので、（括弧）は`TypeError`です。
テスト実行すると、`TypeError`が発生しますが、テスト自体そのエラーが発生することが正となるため、テストはPassします。

## [6章]マーカー
テストを実行する方法を指定できるマーカーという機能があります。先ほどの、[テスト関数をパラメータ化](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/c28013bd-3d83-9cc2-48f9-3f04cfc57517.png)にも取り上げた`@pytest.mark.parametrize`もその1つです。
おすすめマーカーが以下になります。

| マーカー名              | 説明                   | 使用例 | テスト出力 |
|-------------|-------------------------|------------------|------------------|
| `@pytest.mark.skip`     | **条件に関係なくテストをスキップ**       | `@pytest.mark.skip(reason="未実装")` | <img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/33263d55-ee90-b6b9-07bf-22ba02701dca.png"> | 
| `@pytest.mark.skipif`   | **条件付きでスキップ**                   | `@pytest.mark.skipif(sys.version_info < (3, 8), reason="Python 3.8以上のみ")` |<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/882dc182-7f90-2016-16e4-e679a74dd9cd.png"> | 
| `@pytest.mark.xfail`    | **失敗してもテストを通過**               | `@pytest.mark.xfail(reason="バグ修正待ち")` | <img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/c28013bd-3d83-9cc2-48f9-3f04cfc57517.png">| 
| `@pytest.mark.slow`     | **遅いテストの分類（カスタム）**         | `@pytest.mark.slow`（`pytest -m "not slow"` で除外可能） |  | 
| `@pytest.mark.usefixtures`  | **テスト前に指定したフィクスチャを実行** | `@pytest.mark.usefixtures("cleandir")` |


slowを利用する場合は、pytest設定ファイル、`pytest.ini`にて定義をする必要があります。以下項目を追加ください。
```bash
markers = slow: marks tests as slow
```

今回紹介したマーカー以外にもたくさんのマーカーがあります。コマンド`pytest --markers`で一覧を取得できたり、カスタムマーカーを作成できたりもします。詳しくはこちら→[pytestドキュメント](https://docs.pytest.org/en/stable/how-to/mark.html#how-to-mark-test-functions-with-attributes)

:::note info
未実装の箇所をスキップできたり、失敗しても良いテストがあったりとマーカーの機能は特にテスト駆動開発を行うにあたって有効な機能だなという体感を受けました。（初心者感覚ですが🔰）
:::

## まとめ
今回は、pytestのより良い使い方を学ぶことができました。便利な機能が多くある一方で、それらを自身のテストコードに適切に落とし込むのは難しいと感じましたが、テストの担保力を高めるために、引き続き工夫しながら取り組んでいきたいと思います。
また今回テスト駆動についても触れられることができたので、今後は手法を学びつつも、より質の高いテストコードを書けるよう勉強できればと思います。引き続き頑張っていきます〜！