pytestのraisesとfailで例外と失敗を捕捉しよう!

eyecatch-pytest-raises-fail
目次

概要

pytest

pytestは、Python 用のシンプルで柔軟なテストフレームワークです。環境構築が容易で書きやすく、Pythonのテストフレームワークでファーストチョイスになるツールです。下記の記事でpytestの概要について説明していますので興味のある方はご参考ください。

正常系、純正常系、異常系

正常系、準正常系、異常系のテストを網羅することで、システムが意図通りに動作するか、特殊なケースやエラーに対しても安定しているかを確認できます。それぞれの観点について違いを以下で説明します。なお現場によっては純正常系を異常系として定義する場合もあります。わたしは両方の現場を経験しました。

分類定義目的
正常系システムが意図通りに動作することを確認するテストケース通常の利用方法や期待される入力に対して、システムが正しい結果を返すことを確認ユーザーが正しいログイン情報を入力したときに正常にログインできるかどうかをテスト
準正常系やや特殊なケースや境界値に近いケースをテストシステムが期待される範囲内で動作し、ユーザーにとって許容できる挙動を示すかどうかを確認パスワード入力欄に最小文字数や最大文字数のパスワードを入力しても問題が起きないかを確認
異常系意図しない入力や操作に対するシステムの反応を確認するテストケース不正な操作や無効なデータが与えられた際に、システムが適切なエラー処理を行うか確認パスワード入力欄に許容されていない特殊文字を入力した際にエラーメッセージが表示されるか確認

純正常系が少しわかりにくいかもしれませんが、普段使っている範囲でユーザーがちょっとした操作ミスをした場合の挙動と考えるといいと思います。一方で、ボタンを何十回も連打して動作エラーが出た場合は普段使っている範囲とは考えにくいので異常系ととらえることができます。

例外と失敗

テスト実行の際は例外や失敗の発生をしっかり補足することが重要です。例外は通常意図しない動作が行われその結果として発生するものであり、異常系のテストをする場合に重要です。また失敗は文字通りテストが失敗していることを意味しておりテストかテスト対象の処理に何かの不具合があることを意味します。

テスト実施の際は異常系テストやあえて失敗するテスト(例えばまだテストコードが未実装だがテストを通したい場合)を実装することがあるため、その場合にどのようなコードを記述すべきかを以下で説明していきます。

raisesとfailの具体的な実装

pytest.raises

例外発生が正しく動作しているかをチェックするテストをするためにはpytest.raises を利用します。この関数を使って例外を補足する方法としてはwith文で利用する方法関数として利用する方法の2つがあります。

with文で利用する場合

まずはwith文でpytest.raisesを利用する場合を見ていきます。

下記のコードで、4-7行目のdivide()はゼロで割ろうとすると ValueErrorを発生させる関数です。10-12行目のtest_divide_by_zero()では、pytest.raisesを使用してゼロで割ろうとしたときに ValueError が発生するかをテストしています。加えてmatch 引数を使うことで、例外メッセージが期待する文字列と一致するかも確認できます。15行目のtest_divide_normal_case() では、通常の割り算が正しく動作するかどうかもテストしています。

import pytest

# 例外を発生させる関数
def divide(x, y):
    if y == 0:
        raise ValueError("Cannot divide by zero")
    return x / y

# ===== with文を利用する場合 =====
def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(10, 0)
# ===== with文を利用する場合 =====

def test_divide_normal_case():
    assert divide(10, 2) == 5

関数として利用する場合

続いて関数としてpytest.raisesを利用する場合を見ていきます。12行目では例外発生の情報を一度exc_infoに格納しその中にある情報を利用して評価を行っています。ここで12行目で格納されたexc_infoに格納された例外情報について主なものを整理します。

  • exc_info.value:例外オブジェクトそのもの。
  • exc_info.type:発生した例外の型。
  • exc_info.traceback:トレースバック情報(例外発生時のスタックトレース)。
  • exc_info.match():例外メッセージが特定のパターンに一致するかを確認するメソッド。
import pytest

# 例外を発生させる関数
def divide(x, y):
    if y == 0:
        raise ValueError("Cannot divide by zero")
    return x / y

# ===== 関数として利用する場合 =====
def test_divide_by_zero():
    # pytest.raisesを関数として利用
    exc_info = pytest.raises(ValueError, divide, 10, 0)
    assert str(exc_info.value) == "Cannot divide by zero"  # 例外メッセージの確認
# ===== 関数として利用する場合 =====

def test_divide_by_zero_info():
    exc_info = pytest.raises(ValueError, divide, 10, 0)
    
    # 例外のメッセージを確認
    assert str(exc_info.value) == "Cannot divide by zero"
    
    # 例外の型を確認
    assert exc_info.type == ValueError
    
    # 例外のトレースバックを確認
    traceback = exc_info.traceback
    
    # 例外メッセージが特定のパターンに一致するか確認
    exc_info.match("Cannot divide by zero")

with文方式と関数方式の使い分け

上記で説明した2つの方式の使い分けについて説明します。適切に使い分けることによってテストの網羅性や実装効率を向上させることができます。

with文方式は処理の中に複数の例外が発生する可能性がある場合に利用するとよいでしょう。3-13行目のコードのように処理の中に条件分岐で例外が複数実装されている場合はwith方式が咳しています。一方で関数方式ですが、シンプルに例外発生と確認したい場合や発生した例外の詳細を確認したい場合に利用するとよいでしょう。

import pytest

def process_data(data):
    # Step 1: データが空であれば例外を発生
    if not data:
        raise ValueError("Data cannot be empty")
    
    # Step 2: データが文字列でなければ例外を発生
    if not isinstance(data, str):
        raise TypeError("Data must be a string")
    
    # Step 3: データを整数に変換できなければ例外を発生
    return int(data)

# データが空のパターンをテスト    
def test_process_data_with_empty_data():
    with pytest.raises(ValueError, match="Data cannot be empty"):
        # ここで複数のステップを含む関数を実行
        process_data("")

# データが文字列以外のパターンをテスト        
def test_process_data_with_non_string_data():
    with pytest.raises(TypeError, match="Data must be a string"):
        # ここで複数のステップを含む関数を実行
        process_data(1)

pytest.fail

条件分岐で失敗処理が正しく動作しているかをチェックするテストをするためにはpytest.fail() を利用します。特に特定のコードパスに到達してはならないことを明示したい場合や、テストにおける未実装の部分を示したいときに便利です。カスタムメッセージを指定することで失敗理由を明確にできる点も有用です。

下記のコードで、4-7行目のfetch_data_from_api() はAPIのレスポンスをシミュレートしており、レスポンスが None なら “No data” を返します。10-15行目のtest_fetch_data() ではレスポンスが “No data” であった場合、テストを強制的に失敗させ、理由を “API returned no data, expected valid response” として出力します。最後に17-19行目のtest_fetch_data_normal_case() では、正常なデータが返ってきた場合の動作を確認します。

import pytest

# テスト対象の関数
def fetch_data_from_api(response):
    if response is None:
        return "No data"
    return response

# pytestを使ったテストケース
def test_fetch_data():
    # ここで想定外のエラーや条件が発生した場合
    response = fetch_data_from_api(None)
    
    if response == "No data":
        pytest.fail("API returned no data, expected valid response")

def test_fetch_data_normal_case():
    response = fetch_data_from_api("Valid data")
    assert response == "Valid data"

まとめ

本記事ではpytestにおける例外と失敗の捕捉方法について説明しました。テストを実装していると異常系の実装はとても大事です。加えてテストそのものの動作がおかしい場合に明示的な失敗を補足することはテストの修正に役立ちます。本記事で紹介した内容を参考に例外や失敗の捕捉をしっかりコントロールしながらテスト実装の効率を高めてみてください!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

Hack Luck Labの管理人hakula(ハクラ)です。2012年にSIerに新卒入社し、SE、新規事業、情シスを担当。その後、ITコンサルを経て、現在はバックエンドエンジニア。過去にはC#、SQL Server、JavaScriptで開発を行い、現在はPython、Rest Framework、Postgresql、Linux、AWSなどを使用しています。ノーコードツールやDX関連も興味あり。「技術は価値を生むために使う」ことが信条で、顧客や組織への貢献を重視しています。

目次