Python Unittest と Pytest

Salman Mehmood 2023年10月10日
Python Unittest と Pytest

この記事の主な目的は、Python の単体テストで最もよく使用される 2つのフレームワーク、unittest pytest、それぞれの長所と短所、およびどちらを優先するかについて説明することです。

Python unittest vs Pytest

ソフトウェアを作成するときは、開発プロセス全体でエラー チェックのプロセスを維持する必要があります。 ソフトウェアがリリース段階に達すると、その使用中に発生するバグの数が最小限に抑えられます。

Python には、さまざまな入力を与えて動作をチェックすることで、記述されたコードをテストできるさまざまなテスト フレームワークもあります。

エラーが発生した場合は、アプリケーションの最初のリリース後のホットフィックスではなく、開発段階で修正できます。

コード例:

class Calculate:
    def CheckPrime(self, a):
        for i in range(a):
            if a % i:
                return False
        return True

    def CalcFact(self, a):
        if a == 1:
            return a
        else:
            return a * self.fact(a - 1)

上記のコードには、CheckPrimeCalcFact という名前の 2つの関数が含まれています。これらの関数は、その名前から明らかなように、素数をチェックして階乗を計算します。

Calculate メソッドがスムーズに機能することを確認するには、さまざまな出力を与えることによって発生する可能性のあるエラーをチェックすることが不可欠です。

では、どうすればそれができるでしょうか? コードにエラーがないことを確認するために、さまざまなテスト フレームワークを使用してテスト ケースを記述し、その上でコードをテストして、コードの整合性をチェックできます。

多くのテスト フレームワークがありますが、最も広く使用されているのは unittestpytest の 2つです。 以下でそれらを1つずつ調べてみましょう。

unittest フレームワークによる単体テスト

unittest は、Python 標準ライブラリに含まれている単体テスト フレームワークです。 このフレームワークは、単体テスト用の Java フレームワークであるJUnitに触発されました。

unittest の動作について説明する前に、unittest で一般的に使用される用語 (他の関連するフレームワークでも使用される) を知っておくことが不可欠です。

  • テストケース – テストの最小単位 – 通常、単一のテストで構成されます
  • テスト スイート – グループ化されたテスト ケース – 通常は次々に実行されます
  • Test Runner – テストケースとスイートの実行を調整および処理します

unittest フレームワークを使用してテスト ケースを作成する

Python の標準ライブラリには既に unittest が含まれているため、unittest を使用して単体テストを書き始めるために外部モジュールをダウンロードする必要はありません。

unittest モジュールをインポートした後に開始できます。 それでは、前に実行したコードに焦点を当てましょう。

コード例:

class Calculate:
    def CheckPrime(self, a):
        for i in range(a):
            if a % i:
                return False
        return True

    def CalcFact(self, a):
        if a == 1:
            return a
        else:
            return a * self.fact(a - 1)

unittest を使用してテスト ケースを作成するには、特定の構文に従う必要があります。つまり、テスト クラスは unittest.TestCase の子であり、そのメソッドは test_ で始まる必要があります。

次のコードを検討してください。

import unittest


class Calculate:
    def CheckPrime(self, a):
        for i in range(2, a):
            if a % i == 0:
                return False
        return True

    def CalcFact(self, a):
        if a == 1:
            return a
        else:
            return a * self.CalcFact(a - 1)


class TestCalc(unittest.TestCase):
    def test_CheckPrime(self):
        calc = Calculate()

        # Passing different outputs
        self.assertEqual(calc.CheckPrime(2), True)
        self.assertEqual(calc.CheckPrime(3), True)

        self.assertEqual(calc.CheckPrime(4), False)
        self.assertEqual(calc.CheckPrime(80), False)

    def test_CheckFact(self):
        calc = Calculate()

        # Passing different outputs
        self.assertEqual(calc.CalcFact(2), 2)
        self.assertEqual(calc.CalcFact(3), 6)

出力:

PS D:\Unittest> python -m unittest a.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK 

出力から判断すると、すべてのアサーションが成功したため、すべてのテスト ケースが成功したことがわかります。

次に、テスト ケースが失敗するケースを試してみましょう。

def test_CheckFact(self):
    calc = Calculate()

    # Passing different outputs
    self.assertEqual(calc.CalcFact(2), 2)
    self.assertEqual(calc.CalcFact(3), 6)
    # Supposed to throw an error
    self.assertEqual(calc.CalcFact(0), 0)

出力:

PS D:\Unittest> python -m unittest a.py
======================================================================
ERROR: test_CheckFact (a.TestCalc)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\Python Articles\a.py", line 34, in test_CheckFact
    self.assertEqual(calc.CalcFact(0), 0) # Supposed to throw an error
  File "D:\Python Articles\a.py", line 15, in CalcFact
    return a * self.CalcFact(a-1)
  File "D:\Python Articles\a.py", line 15, in CalcFact
    return a * self.CalcFact(a-1)
  File "D:\Python Articles\a.py", line 15, in CalcFact
    return a * self.CalcFact(a-1)
  [The previous line is repeated 974 more times]
  File "D:\Python Articles\a.py", line 12, in CalcFact
    if (a == 1):
RecursionError: maximum recursion depth exceeded in comparison

----------------------------------------------------------------------
Ran 2 tests in 0.004s
FAILED (errors=1)

コードから明らかなように、python -m unittest <name_of_script.py> を使用してスクリプトを実行します。

unittest モジュールは特定の形式で与えられたスクリプト ファイルを処理するため、このコードはテスト クラスのメソッドを呼び出さなくても機能します。

スクリプトには TestCalc が含まれているため、unittest.TestCase の子クラスは Test Runner によって自動的にインスタンス化されます。

インスタンス化の後、テスト メソッドはクラス内で検出され、順番に実行されます。 メソッドが test method と見なされるためには、test_ で始まる必要があります。

テスト メソッドが見つかると、それらは順番に呼び出されます。 この場合、test_CheckPrimetest_CalcFact の両方が呼び出されます。 実装ではアサーションがチェックされ、予期しない動作が発生した場合は出力にエラーがスローされます。

エラーが含まれていたテスト ケースから、コードの記述方法が原因で、CalcFact メソッドで無限再帰が発生し始めたと推測できますが、これはテスト ケースのおかげで修正できるようになりました。

なぜエラーが発生するのか疑問に思っている場合は、初期条件が 1 未満の数値をチェックしていないことが原因です。

unittest フレームワークの長所と短所

unittest を使用する利点のいくつかを以下に示します。

  • Python 標準ライブラリに含まれています
  • 関連するテスト ケースを単一のテスト スイートに昇格
  • スピーディーなテスト収集
  • 正確なテスト時間

unittest には次の欠点があります。

  • わかりにくい場合があります
  • カラー出力なし
  • 冗長すぎる可能性があります

Pytest フレームワークによる単体テスト

unittest とは異なり、Pytest は組み込みモジュールではありません。 個別にダウンロードする必要があります。 ただし、Pytest のインストールは比較的簡単です。 そのためには、pip を使用して次のコマンドを実行します。

pip install pytest

Pytest を使用してテスト ケースを作成する

Pytest を使っていくつかのテストケースを書いてみましょう。 ただし、始める前に、テスト ケースの作成において Pytestunittest とどのように異なるかを見てみましょう。 Pytest で書かれた単体テストの場合、次のことを行う必要があります。

  • 別のディレクトリを作成し、テストするスクリプトを新しく作成したディレクトリに配置します。
  • test_ で始まるか _test.py で終わるファイルにテストを記述します。 例として、test_calc.py または calc_test.py があります。

Pytest を使用してテスト ケース用に記述された次のコードを検討してください。

def test_CheckPrime():
    calc = Calculate()

    # Passing different outputs
    assert calc.CheckPrime(2) == True
    assert calc.CheckPrime(3) == True

    assert calc.CheckPrime(4) == False
    assert calc.CheckPrime(80) == False


def test_CheckFact():
    calc = Calculate()

    # Passing different outputs
    assert calc.CalcFact(2) == 2
    assert calc.CalcFact(3) == 6
    # assert calc.CalcFact(0) == 0 # Supposed to throw an error

出力:

============================================================== test session starts ==============================================================
platform win32 -- Python 3.10.7, pytest-7.1.3, pluggy-1.0.0
rootdir: D:\Unittest
collected 2 items

test_a.py
[100%]

=============================================================== 2 passed in 0.04s ===============================================================

ここで、失敗したテスト ケースを使用します。

============================================================== test session starts ==============================================================
platform win32 -- Python 3.10.7, pytest-7.1.3, pluggy-1.0.0
rootdir: D:\Unittest
collected 2 items

test_a.py .F
[100%]

=================================================================== FAILURES ====================================================================
________________________________________________________________ test_CheckFact _________________________________________________________________

    def test_CheckFact():
        calc = Calculate()

        # Passing different outputs
        assert calc.CalcFact(2) == 2
        assert calc.CalcFact(3) == 6
>       assert calc.CalcFact(0) == 0 # Supposed to throw an error

test_a.py:50: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
test_a.py:13: in CalcFact
    return a * self.CalcFact(a-1)
test_a.py:13: in CalcFact
    return a * self.CalcFact(a-1)
test_a.py:13: in CalcFact
    return a * self.CalcFact(a-1)
.
.
.
.
.
RecursionError: maximum recursion depth exceeded in comparison

test_a.py:10: RecursionError
============================================================ short test summary info ============================================================ 
FAILED test_a.py::test_CheckFact - RecursionError: maximum recursion depth exceeded in comparison
========================================================== 1 failed, 1 passed in 2.42s ========================================================== 

Pytest を使用して記述されたテスト ケースは、unittest よりも少し単純です。 unittest.TestCase の子クラスを作成する代わりに、メソッドの先頭に test_ を付けてテスト関数を書くことができます。

Pytest フレームワークの長所と短所

Python で Pytest フレームワークを使用する利点を次に示します。

  • コンパクトなテスト スイート

  • 最小限の定型コード

  • プラグインのサポート

  • きちんとした適切な出力プレゼンテーション

    また、以下にリストされている欠点もあります。

  • 他のフレームワークと互換性がないことが多い

著者: Salman Mehmood
Salman Mehmood avatar Salman Mehmood avatar

Hello! I am Salman Bin Mehmood(Baum), a software developer and I help organizations, address complex problems. My expertise lies within back-end, data science and machine learning. I am a lifelong learner, currently working on metaverse, and enrolled in a course building an AI application with python. I love solving problems and developing bug-free software for people. I write content related to python and hot Technologies.

LinkedIn

関連記事 - Python Unit Test