LoginSignup
12

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-02-26

はじめに

本稿は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だとAssertionErrorraiseされることになります。
これがPython標準のassert文の機能になります。

3. unittest の assert

では、標準ライブラリであるunittestassertはどうなっているのでしょうか?
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)

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

ただ、この方式は下記の観点であまりイケてないように感じます。

  • ヘルパー関数をすべて把握しておかなければならない点
  • 汎用性、可読性の高いassert文を無視して実装されている点

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

# unittest
assertNotEqual(actual, expected)

# builtin
assert actual != expected

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

4. pytest の assert

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

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自体の書き換え処理が内部的に走っているため、pytestassertは直感的な文を継承しつつリッチなスタックトレースが表示されることになります。

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のユニットテストについて少しずつ投稿していきますのでよろしくお願いします。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12