【Python】なぜpytestを使うのか？Assertion編

## はじめに

本稿はPythonのユニットテストで、「なぜ`pytest`を使うのか？」のシリーズ第一弾になります。

### 本稿の目的
- Python利用者のユニットテストに対する解像度を上げること
- なぜ`pytest`を使うのか、理由を知ること

### 動作環境
Python: 3.10.6  
pytest: 7.1.3  

## 1. Assertionとは？

テストコードを書く際に、頻出と言っても良いものがこのAssertionです。
Assertionとは[IT用語辞典](https://e-words.jp/w/%E3%82%A2%E3%82%B5%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3.html#:~:text=%E3%82%A2%E3%82%B5%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%80%90assertion%E3%80%91%E3%82%A2%E3%82%B5%E3%83%BC%E3%83%88%20%2F%20assert%20%2F%20%E3%82%A2%E3%82%B5%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF&text=%E3%82%A2%E3%82%B5%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A8%E3%81%AF%E3%80%81%E8%A1%A8%E6%98%8E%E3%80%81%E6%96%AD%E8%A8%80,%E3%81%99%E3%82%8B%E4%BB%95%E7%B5%84%E3%81%BF%E3%82%92%E3%82%A2%E3%82%B5%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A8%E3%81%84%E3%81%86%E3%80%82)を引用すると、

> アサーションとは、表明、断言、主張などの意味を持つ英単語。プログラミングにおいて、あるコードが実行される時に満たされるべき条件を記述して実行時にチェックする仕組みをアサーションという。

ということであり、**条件の結果を表明する**仕組みになります。
以降、Python文法に合わせて`assert`と呼称します。

## 2. builtin の assert

Pythonには、[単純文](https://docs.python.org/ja/3/reference/simple_stmts.html#)として、「[assert文](https://docs.python.org/ja/3/reference/simple_stmts.html#the-assert-statement)」が存在しており、プログラムのデバッグ用に用意されています。
`assert`文の記法は

```py
assert {条件式}, {エラーメッセージ}
```

のようになっており、下記のような処理と等価になっています。

```py
if __debug__:
    if not {条件式}:
      raise AssertionError(エラーメッセージ)
```

要するに、条件が`False`だと`AssertionError`が`raise`されることになります。
これが`Python`標準の`assert`文の機能になります。

## 3. unittest の assert

では、標準ライブラリである`unittest`の`assert`はどうなっているのでしょうか？
`unittest`では下記のような`assert`ヘルパー関数が用意されています。

| 関数 ||
| ---- | ----|
| assertTrue | 真のときOK |  
| assertFalse | 偽のときOK |
| assertEqual | 一致するときOK |
| assertNotEqual | 一致しないときOK |
| assertLessEqual | 大小関係的に劣ってるときOK |
| assertIsNone | NoneのときOK |
| assertIsNotNone | NoneではないときOK |

基本的に、この`assert`ヘルパー関数は`assert`と同等の操作が可能です。

```py
# unittest
assertEqual(actual, expected)

# builtin
assert actual == expected
```

ただ、厳密に言えば同等ではないです。というのも`assert`ヘルパー関数は`assert`文を利用しておらず、独立した実装がなされているためです。`assertEqual`の元となっている`_baseAssertEqual`を見てみましょう。

```py
def _baseAssertEqual(self, first, second, msg=None):
    """The default assertEqual implementation, not type specific."""
    if not first == second:
        standardMsg = '%s != %s' % _common_shorten_repr(first, second)
        msg = self._formatMessage(msg, standardMsg)
        raise self.failureException(msg)
```

`builtin`の`assert`よりも例外やロギング周りにテコ入れされていることが分かります。

ただ、この方式は下記の観点であまりイケてないように感じます。
- ヘルパー関数をすべて把握しておかなければならない点
- 汎用性、可読性の高い`assert`文を無視して実装されている点

もう既に思うところはありますが、今度は否定形の`assertNotEqual`と`assert`の違いを見てみましょう。

```py
# unittest
assertNotEqual(actual, expected)

# builtin
assert actual != expected
```

ログや例外の違いはありますが、やはり`builtin`の条件式が簡潔で分かりやすいですね。
`unittest`のようにヘルパー関数を多用する方法は冗長だと思います。
このように感じている方が多かったのでしょうか、次の`pytest`ではこの不満点が解消されています。

## 4. pytest の assert

`pytest`では`builtin`の`assert`文そのものを書き換えします。
`pytest`の`assertion`パッケージを覗いてみると下記のようなフックスクリプトがあります。

```py:rewrite.py
# 分量が多いので処理は割愛しています
class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
    """PEP302/PEP451 import hook which rewrites asserts."""

    def __init__(self, config: Config) -> None:
      ...

    def set_session(self, session: Optional[Session]) -> None:
      ...

    # Indirection so we can mock calls to find_spec originated from the hook during testing
    _find_spec = importlib.machinery.PathFinder.find_spec

    def find_spec(
        self,
        name: str,
        path: Optional[Sequence[Union[str, bytes]]] = None,
        target: Optional[types.ModuleType] = None,
    ) -> Optional[importlib.machinery.ModuleSpec]:
      ...
```

このような`assert`自体の書き換え処理が内部的に走っているため、`pytest`の`assert`は直感的な文を継承しつつリッチなスタックトレースが表示されることになります。

## 5. pytestでのassertの書き方について

少し話したいことがあるため触れます。`assert`は下記ような感じで活用します。

```py
def test_hoge():
  expected = "hoge"
  actual = hoge()
  assert actual == expected
```

また、一つのテストケースに複数の`assert`を記載することが出来ます。

```py
def test_hoge():
  expected = "hoge"
  actual = hoge()
  assert actual == expected
  assert len(expected) == 4
```

ただし、一つのテストケースに複数の`assert`を入れるのことはテストケースの可読性を損なう原因になるため、多用しないことをおすすめします。
基本的に、1テスト1`assert`でどういう結果かを表せるように、1つの関心事に着目した設計を目指しましょう。

ただ、そうは言っても正しく検証するために、複数の`assert`をすべき場合もあります。
そういう場合は、`unittest`で実装されていたような**ヘルパー関数**を作成し、適用するという手もあります。

```py
def assert_deep_equal(actual, expected, length):
  assert actual == expected
  assert len(expected) == length

def test_hoge():
  actual = hoge()
  assert_deep_equal(actual, "hoge", 4)
```

実際にはもっと複雑かつ再利用性のあるものに対し**ヘルパー関数**を作成して利用することになります。

## おわりに

本当は**pytest完全解説**のような記事を作りたかったのですが、完成できる気がしなかったためこのような形で少しずつ小出しに投稿することにしました。
今回で終わりにならないように、`pytest`及びPythonのユニットテストについて少しずつ投稿していきますのでよろしくお願いします。