はじめに
本稿はPythonのユニットテストで、「なぜpytest
を使うのか?」のシリーズ第一弾になります。
本稿の目的
- Python利用者のユニットテストに対する解像度を上げること
- なぜ
pytest
を使うのか、理由を知ること
動作環境
Python: 3.10.6
pytest: 7.1.3
1. Assertionとは?
テストコードを書く際に、頻出と言っても良いものがこのAssertionです。
AssertionとはIT用語辞典を引用すると、
アサーションとは、表明、断言、主張などの意味を持つ英単語。プログラミングにおいて、あるコードが実行される時に満たされるべき条件を記述して実行時にチェックする仕組みをアサーションという。
ということであり、条件の結果を表明する仕組みになります。
以降、Python文法に合わせてassert
と呼称します。
2. builtin の assert
Pythonには、単純文として、「assert文」が存在しており、プログラムのデバッグ用に用意されています。
assert
文の記法は
assert {条件式}, {エラーメッセージ}
のようになっており、下記のような処理と等価になっています。
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
と同等の操作が可能です。
# unittest
assertEqual(actual, expected)
# builtin
assert actual == expected
ただ、厳密に言えば同等ではないです。というのもassert
ヘルパー関数はassert
文を利用しておらず、独立した実装がなされているためです。assertEqual
の元となっている_baseAssertEqual
を見てみましょう。
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
の違いを見てみましょう。
# unittest
assertNotEqual(actual, expected)
# builtin
assert actual != expected
ログや例外の違いはありますが、やはりbuiltin
の条件式が簡潔で分かりやすいですね。
unittest
のようにヘルパー関数を多用する方法は冗長だと思います。
このように感じている方が多かったのでしょうか、次のpytest
ではこの不満点が解消されています。
4. pytest の assert
pytest
ではbuiltin
のassert
文そのものを書き換えします。
pytest
のassertion
パッケージを覗いてみると下記のようなフックスクリプトがあります。
# 分量が多いので処理は割愛しています
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
は下記ような感じで活用します。
def test_hoge():
expected = "hoge"
actual = hoge()
assert actual == expected
また、一つのテストケースに複数のassert
を記載することが出来ます。
def test_hoge():
expected = "hoge"
actual = hoge()
assert actual == expected
assert len(expected) == 4
ただし、一つのテストケースに複数のassert
を入れるのことはテストケースの可読性を損なう原因になるため、多用しないことをおすすめします。
基本的に、1テスト1assert
でどういう結果かを表せるように、1つの関心事に着目した設計を目指しましょう。
ただ、そうは言っても正しく検証するために、複数のassert
をすべき場合もあります。
そういう場合は、unittest
で実装されていたようなヘルパー関数を作成し、適用するという手もあります。
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のユニットテストについて少しずつ投稿していきますのでよろしくお願いします。