おもこん

おもこんは「思いつくままにコンピュターの話し」の省略形です

Python初級者のお勉強ノート(21)Pythonにおけるテスト

プログラム開発において、コンピュータによるテストは欠かせません。 特に複雑なロジックや外部依存が絡む場合、人間の目でバグを発見するのは難しいものです。 それに対して、コンピュータによるテストは高速で正確であり、複雑なプログラムにも対処できます。 本記事では、Pythonでのテストの基本から、実際のテストプログラム例までを解説します。

テストとは何か

テストとは、プログラムが正しく動作していることを確認するために実行される一連の手順です。以下のような目的があります:

  • バグの早期発見: プログラムの想定外の動作を発見する。
  • 変更による影響の確認: コードの変更が既存の機能に悪影響を与えていないことを確認する。
  • コードの信頼性向上: プログラムが設計通りに動作することを保証する。

また、「テスト駆動開発(TDD: Test-Driven Development)」「振る舞い駆動開発(BDD: Behavior-Driven Development)」と呼ばれる、プログラム開発をテストから始める手法もあります。 特に、TDDの進化形である「振る舞い駆動開発」は、ユーザーの視点でシステムの振る舞いを定義し、その振る舞いをテストで確認する開発手法で、さまざまな利点があります。

  • ユーザーの要求に対するプログラムの振る舞いに焦点があたっている。
  • その振る舞いをテストで検証。
  • プログラムを知らないクライアントにも「振る舞い」は理解しやすいため、開発者とクライアントのコミュニケーションがとりやすい。

ここではTDDやBDDまでには踏み込まないので、必要な方はそれぞれの専門書やウェブサイトを参照してください。

テストには以下の種類があります:

  • ユニットテスト: 個々の関数やメソッドの動作を確認するテスト。
  • 統合テスト: 複数のモジュールが正しく連携することを確認するテスト。
  • システムテスト: システム全体が正しく動作することを確認するテスト。

ここではユニットテストにポイントを絞って解説します。

Pythonで用いられる代表的なテストモジュール

Pythonにはテストを支援するためのモジュールがいくつか用意されています。以下は代表的なものです:

  • unittest:

  • pytest:

  • doctest:

    • ドキュメント内のコード例をそのままテストとして実行。
    • 主にサンプルコードの検証に使用。

ここではpytestを説明します。

pytestディレクトリ構成

プロジェクトを以下のような構成にすると、pytestがテストスクリプトを自動検出します。

my_project/
├── sch.py          # テスト対象のプログラム
├── tests/
│   ├── test_sch.py # テストスクリプト
│   └── __init__.py # 空でも良い

テストはtests/ディレクトリに配置し、test_で始まるファイル名にします。

pytest でよく使われるテスト手法(例)

以下の手法は後で紹介するsch.pyのテストプログラムでも使われています。 詳しい説明はプログラムを示した後の解説で行います。

  • フィクスチャ

    • テストに必要な環境(データベース接続、一時ファイルなど)を準備・後片付けする仕組み
    • @pytest.fixtureで定義し、テスト関数に注入できる
  • モック

    • 外部依存(API、DBアクセス、ファイル操作など)を模倣するオブジェクトを用意
    • 実際のリソースを操作せず、想定通りの振る舞いを再現してテストできる
  • 標準出力のキャプチャ

    • capsysフィクスチャを使い、テスト時のprint出力を記録・検証
    • コマンドライン出力が期待通りか簡潔に確認可能

sch.pyのテストプログラム

テストプログラムは以下の機能をテストしています:

  • スケジュールの追加: 新しいスケジュールが正しく保存されるか。
  • スケジュールの一覧表示: 指定した日付のスケジュールが正しく表示されるか。
  • スケジュールの削除: 指定したIDのスケジュールが正しく削除されるか。
import pytest
import sch
import os
import json

TEST_DATA_FILE = "test_schedule.json"

# テスト用のデータファイルを一時的に使用するように変更
@pytest.fixture(autouse=True)
def setup_and_teardown():
    sch.DATA_FILE = TEST_DATA_FILE
    yield
    if os.path.exists(TEST_DATA_FILE):
        os.remove(TEST_DATA_FILE)

def test_add_schedule():
    """スケジュールの追加をテスト"""
    sch.add_schedule("2024-12-10", "10:00", "Meeting")
    sch.add_schedule("2024-12-10", "14:00", "Lunch")
    data = sch.load_data()
    assert len(data) == 2
    assert data[0]["title"] == "Meeting"
    assert data[1]["title"] == "Lunch"

def test_list_schedule(capsys):
    """スケジュールの一覧表示をテスト"""
    sch.add_schedule("2024-12-10", "10:00", "Meeting")
    sch.add_schedule("2024-12-10", "14:00", "Lunch")
    sch.list_schedule("2024-12-10")
    captured = capsys.readouterr()
    assert "Meeting" in captured.out
    assert "Lunch" in captured.out

def test_delete_schedule():
    """スケジュールの削除をテスト"""
    sch.add_schedule("2024-12-10", "10:00", "Meeting")
    sch.add_schedule("2024-12-10", "14:00", "Lunch")
    sch.delete_schedule(1)
    data = sch.load_data()
    assert len(data) == 1
    assert data[0]["title"] == "Lunch"

テストプログラムの解説

  • フィクスチャ:

    • @pytest.fixturePythonのデコレータという機能。デコレータについての説明はここでは省略。別の記事で扱う予定。
    • @pytest.fixtureの次の行に関数(フィクスチャ)を書く。この関数には、テストに必要な環境(データベース接続、一時ファイルなど)を準備・後片付けする仕組みを記述する。
    • セット・アップはyeildの前に書く。yeild自体についての説明はここでは省略。別の記事で扱う予定
    • テスト終了後のティアダウン(クリーンアップともいう)はyeildの後に書く。
    • セットアップでは、sch.pyの定数DATA_FILEをテスト用のファイル名に変更している(sch.pyはインポート時に実行され、DATA_FILE=schedule.jsonになっている。これがセットアップで書き換えられる)。Pythonの定数は実は変数なので書き換えが可能であることに注意。
    • ティアダウンでは、テスト用のファイルが存在したら、それを消去しておく。
  • テストはtest_XXXXの関数が行う

    • ここには3つのテストの関数がある。これらの関数は、特に設定をしなければ、上の関数から順に実行される。
    • 各テスト関数は、フィクスチャのセットアップ後に実行され、実行後はフィクスチャのティアダウンが実行される。
    • それぞれのテストでは以下にのべるアサーションやキャプチャを使う。
  • アサーションを用いた検証:

    • assert文を使って期待する結果と実際の出力を比較する。期待と異なる結果を「失敗(failure)」という。失敗の場合はその結果を画面出力する。
    • assert文は「assert 条件式, [エラーメッセージ]」の形で記述する。
    • 条件がTrueであれば、何も起こらず次のコードが実行され、Falseの場合はAssertionErrorが発生する。エラーメッセージ(オプション)を指定すると、エラー時にメッセージを表示できる。
    • エラーはpytestがキャッチしているため、プロセスが止まることはない。またテストはそれぞれ独立に行われるので、あるテストでエラーが発生しても他のテストに影響はない。
  • 標準出力のキャプチャ:

    • capsysは、(1)テスト前に標準出力(stdout)や標準エラー出力(stderr)を画面などの外部への出力からpytestの内部に取り込む(キャプチャする)ように変更、(2)テスト実行時にそのキャプチャデータを利用、(3)テスト後は出力を元にもどす、ということをする。したがって、これはpytestの提供する一種のフィクスチャである。
    • capsysを使うためにはまず、テスト関数の引数にcapsysを入れる。
    • 出力をキャプチャするにはcapsys.readouterr関数を使う。

テストの実行

  • pytestをインストール: pipを用いる。
PS > pip install pytest
PS > pytest
================================================= test session starts =================================================
platform win32 -- Python 3.13.0, pytest-8.3.4, pluggy-1.5.0
rootdir: C:\Users\(ユーザ名)\Documents\sch
collected 3 items

tests\test_sch.py ...                                                                                            [100%]

================================================== 3 passed in 0.07s ==================================================
PS >
  • 実行結果を確認
    • 3 passed in 0.07sと出力されたので、3つのテスト関数が成功したことがわかる。

まとめ

本記事では、以下の内容を学びました:

  1. テストの基本概念と種類。
  2. Pythonで利用可能な代表的なテストモジュール。
  3. pytestの使い方。
  4. テスト手法(フィクスチャ、出力キャプチャ)とその応用。

pytestを使うことで、テストの記述や実行がシンプルになります。 テストを書くことは難しいことではありません。 プログラムを作成するときは、かならずテストも書くようにしてください。