pytestの基本的な仕組みと書き方を理解しよう!

eyecatch-pytest-basic
目次

概要

pytestとは

pytestは、Python 用のシンプルで柔軟なテストフレームワークです。環境構築が容易で書きやすく、Pythonのテストフレームワークでファーストチョイスになるツールです。私も日々の開発で利用しており、お気に入りのツールの1つとなっています。そんなpytestの利用メリットを下記に述べます。

  • シンプルなテスト記述
    • 関数形式で簡単にテストが書け、クラスやメソッドを明示的に定義する必要がありません。またassert文だけでテスト結果を確認でき、失敗時の詳細なエラーメッセージが自動生成されます。
  • 豊富なプラグイン
    • pytest-cov(テストカバレッジ)、pytest-django(Django 用のテストサポート)など、多数の公式・非公式プラグインが提供され、様々なニーズに対応可能です。
  • 強力なフィクスチャ管理
    • テストのセットアップやクリーンアップが必要な場面で使える「フィクスチャ」という仕組みがあり、コードの重複を防ぎ、テストコードをきれいに保つことができます。
  • パラメータ化
    • 一つのテストケースに複数のデータセットを与え、それぞれに対してテストを実行できるため、テストケースの効率的な管理が可能です
  • テスト自動検出
    • 命名規則に従ったファイルやメソッド(命名にtestやTestを記述)を記述することで自動的にテストケースを検出し簡単にテスト結果を確認することができます。詳細はこちらをご確認ください。

pytestとunittest

Pythonで開発をしているとテスティングフレームワークとしてunittestが利用されているケースもあるでしょう。このunittestはPythonの標準ライブラリとして提供されているため、pipなどでパッケージをインストールせずともPythonが動作する環境であれば利用することができます。

pytestとunittestをの使い分けですが、効率よく陣族にテストを書きたい場合には pytest が非常に有効です。特にパラメータ化やプラグインの活用、テストの簡素化を求める場合に大いにメリットがあります。一方、既存のプロジェクトが unittest を使っている場合や、標準ライブラリを重視するプロジェクトでは、unittest も十分な選択肢です。

pytestとunittestを共存して利用する場合もあるようです。例えばpytestをメインのテスティングフレームワークとして利用し、部分的にunitttestの機能を利用する場合(magicmockを利用するなど)が当てはまります。環境の制約などでpytestの機能が利用できない場合はpytestとunittestを共存させて利用することも検討してみるのもありかもしれません。

テスト実装と実行

環境構築

pythonの仮想環境を利用してpytestの実行環境を構築しましょう。まずはpythonの仮想環境であるvenvにてmyenvディレクトリを作成し仮想環境をアクティベートします。筆者の環境はEC2ですが、アクティベートに成功すると(myenv)のように表示されます。

python -m venv pytest_env
cd pytest_env
source bin/activate
(myenv) [ec2-user@ip-xxx-xxx-xxx-xxx pytest_env]$

pipでpytestをインストールし、バージョンを確認します。バージョンが表示されればインストール完了です。

# pytestインストール
pip install pytest
pytest --version
pytest 8.3.3

# テストファイルを作成(ファイル名にはtest_を付与)
touch test_sample.py

テスト実装と実行

最小限のコード実装と実行

test_sample.pyに下記のようにtest_の接頭語をつけたテストケースとassert文(判定文)を記述します。

厳密にいうとテストの実行だけであれば2行目のインポート文は必要ありませんが、pytestの様々な機能を利用する上では必要になるので記述しておくことが望ましいと思います。またassert文における条件文も書き方ですがはpythonにおいては判定する値 演算子 基準のように書くことが慣習のようです(参考はreal python)。しかしブログ上の記事では逆を推奨していることも多く、プロジェクト内ではどちらかに統一しておくとよいでしょう。

# test_sample.py
import pytest

def test_pass():
  data = (1,2)
  # assert 判定する値 演算子 基準
  assert data == (1,2)

ターミナルでpytestコマンドを実行するとテストコードがpassします。

(pytest_env) [ec2-user@ip-172-31-38-158 pytest]$ pytest 
===== test session starts =====
platform linux -- Python 3.9.16, pytest-8.3.3, pluggy-1.5.0
rootdir: /home/ec2-user/pytest_env/pytest
collected 1 item                                                                                                                               

test_one.py .                                                                                                                            [100%]

===== 1 passed in 0.01s =====

テストケースの書き方

Arrange-Act-Assert (AAA) は、テストのベストプラクティスとして広く採用されているパターンで、テストのコードを「準備」「実行」「検証」の3つの段階に分けて整理します。このパターンは、テストの読みやすさや保守性を高め、バグが発生した際にも容易にデバッグできるという利点があります。テストケースを書く際は下のサンプルコードのようにArrange、Act、Assertのコメント文を入れるようにしています。

似たパターンとしてはGiven-When-Tnenがありますが、要はわかりやすく構造化してテストケースを書くことが重要です。余談ですが、TDD(テスト駆動開発)で有名なケントベックがこのAAAパターンの普及に貢献したらしいです。

def test_addition():
    # Arrange
    a = 2
    b = 3
    
    # Act
    result = a + b
    
    # Assert
    assert result == 5

前処理と後処理の書き方

pytest では、テストの前後に実行される前処理(setup)や後処理(teardown)を指定することができます。これを実現するために、pytest はいくつかの方法を提供しています。関数レベルやクラスレベルの前処理・後処理を指定することで、テスト環境のセットアップやクリーンアップが簡単にできます。

関数ごとの前処理・後処理

まず関数ごとの前処理・後処理のコードを見ていきます。setup_function() は、各テスト関数の実行前に呼び出される関数で、テストに必要なセットアップを行います。teardown_function() は、各テスト関数の実行後に呼び出され、テストで使用したリソースのクリーンアップなどを行います。

# 前処理
def setup_function(function):
    print(f"Setup for {function.__name__}")
    # ここで必要な初期化処理などを行う

# 後処理
def teardown_function(function):
    print(f"Teardown for {function.__name__}")
    # ここで後片付け処理などを行う

# テスト関数
def test_example():
    print(Running test_example: assert 1 + 1 == 2)
    assert 1 + 1 == 2
# コンソールの表示結果
Setup for test_example
Running test_example: assert 1 + 1 == 2
Teardown for test_example

クラス内の関数ごとの前処理・後処理

続いてクラスベースの前処理・後処理のコードを見ていきます。setup_method() は各メソッドの実行前に呼ばれ、テストメソッドごとに共通のセットアップを行います。teardown_method() は各メソッドの実行後に呼ばれ、メソッドで使用したリソースの解放や状態のリセットを行います。

class TestClassExample:

    # 前処理
    def setup_method(self, method):
        print(f"Setup for {method.__name__}")
        # ここで必要な初期化処理を行う

    # 後処理
    def teardown_method(self, method):
        print(f"Teardown for {method.__name__}")
        # ここでクリーンアップ処理を行う

    # テストメソッド
    def test_addition(self):
        print(Running test_addition: assert 2 + 3 == 5)
        assert 2 + 3 == 5

    def test_subtraction(self):
        print(Running test_subtraction: assert 5 - 3 == 2)
        assert 5 - 3 == 2

コンソールの表示結果を確認すると、クラス内に定義しているメソッドごとに前処理と後処理が実行されていることがわかります。

# コンソールの表示果
Setup for test_addition
Running test_addition: assert 2 + 3 == 5
Teardown for test_addition
Setup for test_subtraction
Running test_subtraction: assert 5 - 3 == 2
Teardown for test_subtraction

クラス全体の前処理・後処理

最後にクラス全体の前処理・後処理のコードを見ていきます。これを実現するためにクラスメソッドを定義するデコーダーである@classmethodを利用します。setup_class は、クラス内のすべてのテストメソッドが実行される前に一度だけ実行されます。teardown_class は、クラス内のすべてのテストメソッドが実行された後に一度だけ実行されます。各テストメソッド (test_addition, test_subtraction) は、通常通り個別に実行されますが、クラスレベルのセットアップとクリーンアップが一度だけ行われます。

class TestClassWithClassMethods:

    # クラス全体の前処理
    @classmethod
    def setup_class(cls):
        print("Class setup: Runs once before all test methods")

    # クラス全体の後処理
    @classmethod
    def teardown_class(cls):
        print("Class teardown: Runs once after all test methods")

    # テストメソッド1
    def test_addition(self):
        print(Running test_addition: assert 2 + 2 == 4)
        assert 2 + 2 == 4

    # テストメソッド2
    def test_subtraction(self):
        print(Running test_subtraction: assert 5 - 3 == 2)
        assert 5 - 3 == 2

コンソールの表示結果を確認すると、クラスとして1回だけ前処理、後処理が行われていることがわかります。

# コンソールの表示結果
Class setup: Runs once before all test methods
Running test_addition: assert 2 + 2 == 4
Running test_subtraction: assert 5 - 3 == 2
Class teardown: Runs once after all test methods

まとめ

本記事ではpytestと基本的な書き方について解説をしました。実際のpytestの実装では本記事で紹介したことをベースにフィクスチャ、マーカー、モック、パラメータなどを扱うことになります。ぜひ本記事を利用してpytestの基礎を理解してみてくださいね!

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

この記事を書いた人

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

目次