閉じる

【Python】assertでテスト入門:書き方・落とし穴・実践例

Pythonでのテスト入門としてよく登場するのがassert文です。

テストフレームワークを使う前の軽量なチェックから、pytestによる本格的なテストまで、assertは幅広く活躍します。

しかし、書き方を誤ると本番環境で意図せず無効化されたり、バグを見逃す原因にもなります。

この記事では、Pythonのassertの基本から落とし穴、実践的な使い方まで、図解を交えながら丁寧に解説します。

Pythonのassertとは何か

assert文の基本構文と使い方

Pythonのassertは、「プログラムのある地点で、ある条件が必ず成り立っているはずだ」という前提をコードで表現するための文です。

条件が成り立たない場合、PythonはAssertionErrorを発生させて処理を止めます。

基本構文はとてもシンプルです。

Python
# 基本構文
assert 条件式          # 条件式がFalseならAssertionError
assert 条件式, メッセージ  # 条件式がFalseならメッセージ付きでAssertionError

簡単な例を見てみます。

Python
def average(numbers):
    # リストが空でないことをチェック
    assert len(numbers) > 0, "numbersは空であってはいけません"
    return sum(numbers) / len(numbers)


if __name__ == "__main__":
    print(average([1, 2, 3]))   # 正常: 2.0 が出力される
    print(average([]))          # AssertionError が発生する

上のコードでは、リストnumbersが空でないことを前提としています。

もし空リストが渡されたら、即座にバグとして検出できるように、assertで条件を表現しています。

assertとif文の違い

assertはif文の代わりとして使えるように見えますが、役割が根本的に異なります。

  • if文:
    ユーザー入力や外部からのデータなど、予想通りでないことが普通にありうる状況を扱います。条件がFalseの場合は、代替処理やエラーメッセージ表示など、通常のアプリケーションロジックで対応します。
  • assert:
    「ここまでコードが進んでいれば、この条件は必ずTrueになっているはず」といった、開発者の前提(不変条件)を表現します。Falseになったら、それは想定していないバグであり、アプリケーションを即座に止めて良いものとして扱います。

したがって、ユーザー入力のチェックや業務ルールの検証にassertを使うのは誤りです。

これらはif文や例外処理を使って、意図通りにハンドリングされるべきものです。

テストでassertを使うメリット

テストコードにおいてassertを使う最大のメリットは、「前提条件」と「期待結果」をそのまま記述できることです。

これは次のような利点につながります。

1つ目に、テストコードが非常に短く書けます。

assert func(x) == 10という1行で、「この入力のとき結果は10であるべき」という意図が明確に伝わります。

2つ目に、pytestなどのテストフレームワークはassertを解析して、失敗時に期待値と実際の値を分かりやすく表示してくれます。

特別なアサーション関数を覚えなくても、Python標準のassertだけで多くのテストが書けることは大きな利点です。

3つ目に、assertはあくまで「テストやデバッグのための仕組み」として設計されているため、本番環境では無効化できるという性質があります。

これ自体は落とし穴にもなりますが、「テストや開発時には厳しくチェックし、本番ではパフォーマンスを優先する」という方針を取る場合には有効に働きます。

assertの書き方

シンプルなassertの書き方

もっとも基本的なassertの使い方は、True/Falseを返す条件式をそのまま書く方法です。

Python
def is_adult(age: int) -> bool:
    return age >= 20


if __name__ == "__main__":
    age = 25
    # ここでは、ageは20歳以上であることを前提にしている
    assert is_adult(age)

    # 以下の処理は、ageが成人であることを前提に進む
    print("成人向けサービスを表示します")

このように、「この時点でこの条件は必ず成り立つはず」という箇所にassertを書きます。

条件式には比較演算子だけでなく、関数の戻り値なども使えますが、副作用のないものに限る点には後ほど触れます。

エラーメッセージ付きassertの書き方

テストやデバッグでは、失敗したときに理由がすぐ分かることが重要です。

そのため、assertには説明メッセージを付けておくことが推奨されます。

Python
def divide(a: float, b: float) -> float:
    # bが0でないことを前提にしている
    assert b != 0, "bは0であってはいけません(ゼロ除算防止)"
    return a / b


if __name__ == "__main__":
    print(divide(10, 2))
    print(divide(10, 0))  # AssertionError: bは0であってはいけません(ゼロ除算防止)

メッセージは「何が期待されていたのか」「何が間違っていたのか」が分かるように書くと、原因調査が格段に楽になります。

複数条件を組み合わせたassertの例

1行で複数の条件をチェックしたい場合、andorを使って条件式を組み合わせることができます。

Python
def register_score(name: str, score: int) -> None:
    # 名前が空でなく、スコアが0〜100の範囲であることを前提とする
    assert name, "nameは空文字であってはいけません"
    assert 0 <= score <= 100, "scoreは0〜100の範囲である必要があります"

    print(f"{name}さんのスコア: {score}点を登録しました")


if __name__ == "__main__":
    register_score("Taro", 80)
    register_score("", 50)      # nameのassertで失敗
    register_score("Hanako", 150)  # scoreのassertで失敗

また、1つのassertでまとめてチェックすることも可能です。

Python
def register_score(name: str, score: int) -> None:
    assert name and 0 <= score <= 100, (
        "不正な引数: nameは非空、scoreは0〜100である必要があります"
    )
    print(f"{name}さんのスコア: {score}点を登録しました")

しかし、あまり条件を詰め込み過ぎると内容が読みにくくなります。

テストやデバッグのしやすさを考えると、条件ごとにassertを分ける方が有利な場合も多いです。

関数の戻り値を検証するassertの書き方

テストにおいては、「関数が正しい戻り値を返しているか」を検証することが重要です。

assertを使えば、期待される戻り値をシンプルにチェックできます。

Python
def add(a: int, b: int) -> int:
    return a + b


def test_add():
    # 期待される戻り値をassertで検証する
    assert add(1, 2) == 3
    assert add(-1, 1) == 0
    assert add(0, 0) == 0


if __name__ == "__main__":
    # 簡易テストとして関数を実行
    test_add()
    print("test_addはすべて成功しました")

戻り値の型や構造をチェックしたい場合もあります。

Python
def get_user(user_id: int) -> dict:
    # 実際にはDBなどから取得する想定
    return {"id": user_id, "name": "Taro", "age": 30}


def test_get_user():
    user = get_user(1)
    assert isinstance(user, dict), "戻り値はdictである必要があります"
    assert "id" in user and "name" in user and "age" in user, "必要なキーが不足しています"
    assert user["id"] == 1, "idが期待と異なります"

このように、テスト関数の中で戻り値をassertすることで、関数の契約(どのような値を返すべきか)を明確に表現できます。

assertの落とし穴と注意点

最適化(-Oオプション)でassertが無効化される問題

Pythonのassertで最も重要な注意点が、最適化オプション-Oで実行すると無効化されるという仕様です。

次のようなコードを考えます。

Python
def divide(a: float, b: float) -> float:
    # bが0でないことを前提にしている
    assert b != 0, "bは0であってはいけません"
    return a / b


if __name__ == "__main__":
    print(divide(10, 0))

通常は、ゼロ除算を試みた時点でAssertionErrorになります。

しかし、-Oオプションを付けて実行すると、状況が変わります。

Shell
python -O example.py

この場合、assert b != 0の行は完全に無視され、Pythonはその次のreturn a / bを実行しようとします。

結果として、ZeroDivisionErrorが発生します。

つまり、assertは「消えてもアプリケーションの挙動が変わらない」ことを前提に使わなければならないということです。

ビジネスロジックや入力チェックをassertに頼るのは危険です。

ビジネスロジックにassertを使ってはいけない理由

ビジネスロジックとは、アプリケーションが提供する本来の機能を指します。

たとえば、「在庫が足りなければ注文を拒否する」や「合計金額がマイナスになってはいけない」といった業務上のルールがこれに当たります。

これらをassertで書いてしまうと、本番環境で-Oオプションを使った瞬間に無効化される危険があります。

たとえば、次のようなコードはよくありません。

Python
def create_order(user, item, quantity):
    # 悪い例: ビジネスロジックをassertで書いている
    assert quantity > 0, "数量は1以上でなければなりません"
    # ...注文を登録する処理...

本来は、if文と例外、あるいはバリデーションエラーの返却として実装すべきです。

Python
class ValidationError(Exception):
    pass


def create_order(user, item, quantity):
    if quantity <= 0:
        # ビジネスロジックに関わるチェックは通常のエラーとして扱う
        raise ValidationError("数量は1以上でなければなりません")
    # ...注文を登録する処理...

assertは「バグ検出」「デバッグ支援」のためだけに使い、ビジネスルールの実装には使わないという線引きを徹底することが重要です。

副作用のある式をassertに書く危険性

もう1つの重要な落とし穴が、assertの中で副作用を持つ処理を行ってはいけないという点です。

副作用とは、ファイル書き込みやDB更新、状態の変更などを指します。

例えば、次のようなコードは非常に危険です。

Python
def save_user(user):
    # 悪い例: 副作用のある関数をassertの中で呼び出している
    assert save_to_db(user), "DB保存に失敗しました"

通常実行ではsave_to_db(user)が呼び出され、DBに保存されます。

しかし-Oオプションで最適化した場合、このassert行はまるごと削除されます。

つまり、DB保存自体が行われません。

正しい書き方は、副作用とassertを分離することです。

Python
def save_user(user):
    # 先に副作用を実行する
    result = save_to_db(user)
    # その結果が期待どおりかをassertでチェックする(あくまでデバッグ用)
    assert result, "DB保存に失敗しました"

このように、assertの中には「状態を変えない式」だけを書くことが重要です。

assertのメッセージにf文字列を使うときの注意点

assertのメッセージにf文字列を使うのは、失敗時の情報を分かりやすくする上で非常に有用です。

しかし、ここにも1つ注意点があります。

Python
# 悪い例: f文字列の中で重い処理をしている
assert x > 0, f"xは正の数である必要があります。現在の統計情報: {expensive_calc()}"

Pythonでは、assert文自体が無効化されても、メッセージ用の式評価は行われる状況があり得ます。

実際にはCPythonの最適化である程度抑えられますが、特にログ用に重い処理をf文字列の中に書くのは避けた方が安全です。

より安全な書き方として、次のようなパターンがあります。

Python
def debug_stats():
    # 重い処理: デバッグ用途のみ
    return expensive_calc()


# 条件がFalseのときだけメッセージ組み立てを行う
if not x > 0:
    debug_message = f"xは正の数である必要があります。現在の統計情報: {debug_stats()}"
    assert False, debug_message

多くの場合、素直にf"x must be positive, got {x}"程度の軽いメッセージで十分です。

重い処理をメッセージ内で行わないように心がけましょう。

本番環境とテスト環境でのassertの扱い方

assertは、本番環境での挙動をどう設計するかによって、使い方の方針が変わります。

テスト・開発環境では、通常-Oを付けずに実行し、assertをフルに活用してバグを早期検出するのがおすすめです。

特に、テストコード内のassertは積極的に使って問題ありません。

一方、本番環境では次の2つの方針があります。

1つ目に「-Oを付けずに実行し、assertも有効にしておく」パターンです。

この場合、予期しないバグが起きたときに即座に止めるという保険としてassertが働きます。

パフォーマンスへの影響は通常小さいため、多くのWebアプリケーションではこの方針が現実的です。

2つ目に「-Oを付けて実行し、assertを完全に無効化する」パターンです。

この場合、assertに一切ビジネスロジックを含めないことが絶対条件です。

また、テストコードにだけassertを使い、本番コード側ではビジネスロジックの検証にif文と例外を用いるという明確な住み分けが重要になります。

assertで始めるPythonテスト実践例

既存コードにassertで簡易テストを追加する

まずは、既存のスクリプトに軽量なテストを追加する方法から見ていきます。

小さなスクリプトでは、ファイルの末尾にテスト関数を追加してassertで検証するだけでも、バグの混入をかなり防げます。

Python
# calculator.py

def add(a: int, b: int) -> int:
    return a + b


def sub(a: int, b: int) -> int:
    return a - b


def mul(a: int, b: int) -> int:
    return a * b


def div(a: int, b: int) -> float:
    if b == 0:
        raise ValueError("bは0以外である必要があります")
    return a / b


def _run_simple_tests():
    # addのテスト
    assert add(1, 2) == 3
    assert add(-1, 1) == 0

    # subのテスト
    assert sub(5, 3) == 2
    assert sub(0, 3) == -3

    # mulのテスト
    assert mul(2, 3) == 6
    assert mul(-1, 3) == -3

    # divのテスト
    assert div(6, 3) == 2
    try:
        div(1, 0)
        assert False, "div(1, 0)は例外を投げるべきです"
    except ValueError:
        pass  # 期待通り


if __name__ == "__main__":
    _run_simple_tests()
    print("簡易テストはすべて成功しました")

このように、内部用のテスト関数を用意して、__main__ブロックから呼び出すだけでも、将来のリファクタリングで安心感が増します。

ユニットテスト関数をassertだけで書いてみる

本格的なテストフレームワークを使わなくても、テスト関数 + assertだけでシンプルなユニットテストを構成できます。

Python
# math_utils.py

def square(x: int) -> int:
    return x * x


def is_even(n: int) -> bool:
    return n % 2 == 0


def test_square():
    assert square(2) == 4
    assert square(-3) == 9
    assert square(0) == 0


def test_is_even():
    assert is_even(2) is True
    assert is_even(3) is False
    assert is_even(0) is True


if __name__ == "__main__":
    # テスト関数を手動で呼び出す
    test_square()
    test_is_even()
    print("すべてのテストが成功しました")

このスタイルの良い点は、そのままpytestのテストとしても使えることです。

後からテストフレームワークを導入したくなったときの移行コストが非常に低くなります。

pytestでのassert活用例

pytestは、Pythonのassert文をそのまま利用するテストフレームワークです。

追加のアサーションメソッドを覚える必要がなく、普段通りのPythonの書き方でテストを記述できます。

Python
# test_calculator.py

from calculator import add, sub, mul, div
import pytest  # pytestを利用する例だが、assert自体は標準のもの


def test_add():
    assert add(1, 2) == 3
    assert add(-1, 1) == 0


def test_sub():
    assert sub(5, 3) == 2
    assert sub(0, 3) == -3


def test_mul():
    assert mul(2, 3) == 6
    assert mul(-1, 3) == -3


def test_div():
    assert div(6, 3) == 2
    with pytest.raises(ValueError):
        div(1, 0)

このテストを実行するには、コマンドラインからpytestを実行します。

Shell
pytest

pytestはassertを解析し、例えばassert add(1, 2) == 4という誤った期待値があった場合、どの値がどのように違っていたかを詳細に表示してくれます。

例外発生をassertでテストする方法

例外が正しく発生するかどうかも、テスト対象として重要です。

生のassertだけで例外までチェックしようとすると、少しコードが冗長になります。

Python
from calculator import div


def test_div_raises():
    try:
        div(1, 0)
        # ここに到達したら例外が出ていないので失敗
        assert False, "div(1, 0)はValueErrorを投げるべきです"
    except ValueError:
        # 期待通り
        pass

pytestを使う場合は、pytest.raisesを使うと非常にスッキリ書けます。

Python
import pytest
from calculator import div


def test_div_raises():
    with pytest.raises(ValueError):
        div(1, 0)

「例外が発生すること自体が期待される振る舞い」であるケースでは、このようにテストで明示的に検証しておくと安心です。

テストコードと本番コードを分けるディレクトリ構成例

テストが増えてきたら、本番コードとテストコードをディレクトリ単位で分けるのがおすすめです。

以下は、よく使われる構成例です。

my_project/
├── src/
│   └── myapp/
│       ├── __init__.py
│       ├── calculator.py
│       └── math_utils.py
├── tests/
│   ├── test_calculator.py
│   └── test_math_utils.py
└── pyproject.toml (または setup.cfg、pytest.ini など)

本番コード側には、ビジネスロジックやAPIクライアントなど、実際にアプリケーションで使われる機能を配置します。

一方、testsディレクトリにはテスト専用のコードだけを置くことで、責務の分離が明確になり、見通しが良くなります。

テストコードの中では、これまで見てきたようなassertを使った検証を行い、pytestなどのフレームワークを使って一括実行します。

テストと本番を分けておけば、本番環境ではテストコードごとデプロイしないという選択も取りやすくなります。

まとめ

Pythonのassertは、「ここではこの条件が必ず成り立つはずだ」という開発者の前提をコードで表すための仕組みです。

シンプルな構文でテストやデバッグに大きく貢献してくれますが、-Oオプションで無効化されるという性質から、ビジネスロジックや副作用を含めてはいけません。

テストコードではassertを積極的に活用しつつ、本番コードではif文と例外で業務ルールを実装するという住み分けを徹底すると、安全で読みやすいコードになります。

まずは小さな関数から、assertを使ったテストを日々の開発に取り入れてみてください。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!