Python Blog

pytestのfixture入門|テスト前の準備処理をわかりやすく解説

| #python #学習

pytestのfixtureは、テスト前の準備処理を再利用しやすくする仕組みです。基本の書き方から後片付け、scopeやconftest.pyの使い方まで初心者向けに解説します。

Pythonでテストを書き始めると、最初はassertだけでも十分に感じます。 ただ、少しコードが増えてくると、毎回同じデータを作ったり、同じ準備処理を書いたりしていませんか。

そのときに役立つのが、pytestのfixtureです。 fixtureを使うと、テスト前に必要なデータや状態をまとめて用意でき、テストコードを読みやすく保てます。

この記事では、pytestのfixtureをIT初心者にもわかりやすく解説します。 pytestの全体像から知りたい方は、以下の記事からご覧ください。 【関連記事】pytest入門|Pythonでテストを書く基本を初心者向けに解説

pytestのfixtureとは?

まずはfixtureの役割を整理しましょう。 名前だけ見ると難しそうですが、考え方はとてもシンプルです。

fixtureとは、テストを実行する前に必要な準備をまとめておく仕組みです。 たとえば、テスト用のユーザーデータを作る、空のリストを用意する、一時ファイルを作る、といった処理に使えます。

テストでは、確認したい処理そのものに集中したいですよね。 毎回データ作成のコードが長く並んでいると、何をテストしているのかが見えにくくなります。

fixtureは、その見えにくさを減らしてくれます。 準備処理を別の場所に切り出すことで、テスト本体がすっきりします。

fixtureが必要になる場面

最初はfixtureなしでも問題ありません。 ただ、同じような準備コードが何度も出てきたら、fixtureを検討するタイミングです。

たとえば、ユーザー情報を扱うテストで、毎回同じ辞書データを作っているとします。 小さなコードなら気になりませんが、実務ではデータがもっと複雑になります。

def test_user_name():
    user = {"name": "Taro", "age": 20}
    assert user["name"] == "Taro"


def test_user_age():
    user = {"name": "Taro", "age": 20}
    assert user["age"] == 20

この例では、userの作成が重複しています。 ここをfixtureにすると、準備処理を1か所にまとめられます。

fixtureの基本的な書き方

fixtureは、@pytest.fixtureを関数につけて定義します。 テスト関数の引数にfixture名を書くと、pytestが自動でそのfixtureを呼び出してくれます。

ここが少し不思議に感じるかもしれません。 でも、慣れると自然に読めるようになります。

import pytest


@pytest.fixture
def sample_user():
    return {"name": "Taro", "age": 20}


def test_user_name(sample_user):
    assert sample_user["name"] == "Taro"


def test_user_age(sample_user):
    assert sample_user["age"] == 20

sample_userというfixtureを用意し、テスト関数の引数として受け取っています。 テスト関数の中で直接呼び出していないのに使えるのがポイントです。

pytestは、テスト関数の引数名を見て、同じ名前のfixtureを探します。 そして、テスト実行前にfixtureを実行し、その戻り値を引数として渡してくれます。

fixtureは普通のPython関数に近い

fixtureという名前だけ聞くと特別に見えますが、中身はほぼ普通の関数です。 returnで値を返せますし、リストや辞書、クラスのインスタンスも返せます。

@pytest.fixture
def empty_cart():
    return []


def test_add_item(empty_cart):
    empty_cart.append("apple")
    assert empty_cart == ["apple"]

このように、テストに必要な初期状態を作るのに向いています。 テストのたびに新しい空のカートが渡されるので、他のテストの影響を受けにくくなります。

fixtureを使うメリット

fixtureのメリットは、単にコードを短くすることだけではありません。 テストを長く運用していくと、読みやすさや変更しやすさの差がじわじわ効いてきます。

同じ準備を何度も書かないための置き場所。 | 観点 | fixtureなし | fixtureあり | | --- | --- | --- | | 準備コード | 各テストに重複しやすい | 1か所にまとめやすい | | 読みやすさ | 確認内容が埋もれやすい | テストの意図に集中しやすい | | 変更への強さ | 修正箇所が増えやすい | fixtureだけ直せることが多い | | 実務での保守 | テストが増えるほどつらい | 共通化しやすい |

表で見ると、fixtureの目的が少し見えやすくなります。

fixtureは早めに学ぶと楽になる

私の経験では、fixtureを理解している人のテストは読みやすいです。 逆にfixtureを避けたままテストが増えると、同じような準備コードがあちこちに散らばり、どこを直せばよいのかわからなくなりがちです。

特に実務では、入力データ、設定値、外部サービスの代わりになる処理など、準備が必要なものが増えていきます。 そのたびにテスト関数へ直接書き込むと、テストの本題が見えなくなります。

最初から完璧に使いこなす必要はありません。 まずは、よく使うテストデータを1つfixtureに切り出すだけで十分です。

fixtureでテストデータを準備する

次に、もう少し実践的な例を見てみましょう。 ここでは、商品の合計金額を計算する関数をテストします。

# shop.py

def calculate_total(items):
    return sum(item["price"] for item in items)

この関数は、商品のリストを受け取り、priceの合計を返します。 次に、この関数をfixtureで用意したデータを使ってテストします。

# test_shop.py
import pytest
from shop import calculate_total


@pytest.fixture
def sample_items():
    return [
        {"name": "apple", "price": 120},
        {"name": "banana", "price": 80},
        {"name": "orange", "price": 100},
    ]


def test_calculate_total(sample_items):
    assert calculate_total(sample_items) == 300

このコードでは、商品のリストをsample_itemsというfixtureにまとめています。 テスト関数は、合計金額が300になることだけに集中できます。

データの中身が増えても、テスト本体はあまり変わりません。 これがfixtureの使いやすいところです。

テストデータの意味が名前で伝わる

fixture名は、テストの読みやすさに大きく影響します。 datasampleだけでは、何のデータなのか少しわかりにくいですよね。

sample_itemsactive_userempty_cartのように、役割がわかる名前をつけると読みやすくなります。 名前を見ただけで、テストの前提が伝わるからです。

@pytest.fixture
def active_user():
    return {"name": "Taro", "status": "active"}


@pytest.fixture
def inactive_user():
    return {"name": "Jiro", "status": "inactive"}

【関連記事】pytestのassert文の使い方解説|Pythonテストで期待値を確認する基本

fixtureで前処理と後処理を書く

fixtureは、データを返すだけではありません。 テスト前に準備し、テスト後に片付ける処理も書けます。

たとえば、一時的にファイルを作ってテストしたい場合があります。 テスト後にそのファイルを消したいなら、yieldを使うと便利です。

import pytest


@pytest.fixture
def temp_text_file(tmp_path):
    file_path = tmp_path / "sample.txt"
    file_path.write_text("hello", encoding="utf-8")

    yield file_path

    if file_path.exists():
        file_path.unlink()


def test_read_file(temp_text_file):
    text = temp_text_file.read_text(encoding="utf-8")
    assert text == "hello"

yieldより前がテスト前の準備です。 yieldでテストに値を渡し、yieldより後がテスト後の後片付けになります。

この流れを覚えると、ファイル、DB接続、設定変更などの後片付けが必要な処理にも対応しやすくなります。

tmp_pathはpytestが用意してくれる便利なfixture

上の例のtmp_pathは、pytestが標準で用意している一時ディレクトリ用fixtureです。 ファイル処理のテストで、他のテストに影響しにくい安全な場所を使えます。

fixtureのscopeを理解する

fixtureには、どのタイミングで作られるかを指定するscopeがあります。 毎回作るのか、ファイルごとに作るのか、テスト全体で1回だけ作るのかを決められます。

少し実務寄りですが、知っておくとテストの速度や安定性に関わります。 まずは一覧で見てみましょう。

scope 実行される単位 よく使う場面
function テスト関数ごと 基本。迷ったらこれ
class テストクラスごと クラス単位で共有したいとき
module テストファイルごと ファイル内で重い準備を共有したいとき
package パッケージごと 複数モジュールでまとめたいとき
session テスト実行全体で1回 重い初期化を1回だけ行いたいとき

何も指定しない場合は、基本的にfunctionです。 つまり、テスト関数ごとにfixtureが作られます。

@pytest.fixture(scope="module")
def tax_rate():
    return 0.1

scopeを広げるときの注意点

scopeを広げると、準備処理の回数を減らせます。 ただし、共有されたオブジェクトを書き換えると、別のテストに影響する可能性があります。

@pytest.fixture(scope="module")
def shared_list():
    return []


def test_append_a(shared_list):
    shared_list.append("a")
    assert shared_list == ["a"]


def test_append_b(shared_list):
    shared_list.append("b")
    assert shared_list == ["b"]

このコードは、実行順によって失敗する可能性があります。 初心者のうちはfunctionを基本にして、必要なときだけscopeを広げるのがおすすめです。

conftest.pyでfixtureを共有する

テストが増えてくると、複数のテストファイルで同じfixtureを使いたくなります。 そのときに便利なのがconftest.pyです。

conftest.pyにfixtureを書いておくと、同じディレクトリ配下のテストから使えるようになります。 各テストファイルでimportしなくても使える点が特徴です。

project/
├── app.py
└── tests/
    ├── conftest.py
    ├── test_user.py
    └── test_order.py

たとえば、tests/conftest.pyに共通fixtureを定義します。 ユーザー情報を複数のテストで使いたい場合に便利です。

# tests/conftest.py
import pytest


@pytest.fixture
def test_user():
    return {"id": 1, "name": "Taro"}

そして、別のテストファイルからそのまま使えます。 明示的にimportしないので、最初は不思議に感じるかもしれません。

# tests/test_user.py

def test_user_name(test_user):
    assert test_user["name"] == "Taro"

conftest.pyは便利ですが、何でも入れる場所ではありません。 本当に複数ファイルで共有したいfixtureだけを置くと、見通しがよくなります。

autouse=Trueは便利だが慎重に使う

fixtureには、テスト関数の引数に書かなくても自動で実行されるautouse=Trueという指定があります。 毎回必ず実行したい準備処理には便利ですが、初心者のうちは使いすぎに注意しましょう。

import pytest


@pytest.fixture(autouse=True)
def show_start_message():
    print("test start")


def test_sample():
    assert 1 + 1 == 2

この例では、引数に何も書いていなくてもfixtureが実行されます。 見えない処理が増えるため、使いどころは絞りましょう。

fixtureでよくあるつまずき

fixtureを初めて使うと、いくつかハマりやすいポイントがあります。 エラーが出ても、仕組みを知っていれば落ち着いて対応できます。

まず多いのは、fixture名とテスト関数の引数名が一致していないケースです。 pytestは引数名を見てfixtureを探すので、名前が違うと見つけられません。

@pytest.fixture
def sample_user():
    return {"name": "Taro"}


def test_user(user):
    assert user["name"] == "Taro"

この例では、fixture名はsample_userなのに、テスト関数ではuserを受け取っています。 正しくは、次のように名前を合わせます。

def test_user(sample_user):
    assert sample_user["name"] == "Taro"

もう1つの注意点は、fixtureを普通の関数のように直接呼び出さないことです。 fixtureはpytestに呼び出してもらう前提の仕組みです。

# よい例

def test_user(sample_user):
    assert sample_user["name"] == "Taro"

pytestらしい書き方に慣れるまでは少し違和感があります。 でも、この形にしておくと、scopeや後処理、他のfixtureとの連携が使いやすくなります。

Pythonプロジェクト全体の設定管理を学びたい方は、以下の記事もあわせて読むと役立ちます。 【関連記事】pyproject.tomlとは?これからのPythonプロジェクト管理入門

まとめ

pytestのfixtureは、テスト前の準備処理を整理するための便利な仕組みです。 テストデータを用意したり、一時ファイルを作ったり、後片付けをしたりと、さまざまな場面で使えます。

最初に覚えるべきポイントは、@pytest.fixtureで定義し、テスト関数の引数として受け取ることです。 この基本だけでも、重複した準備コードをかなり減らせます。

慣れてきたら、yieldによる後処理、scopeによる実行タイミングの調整、conftest.pyによる共有へ進むとよいでしょう。 ただし、最初から全部を使いこなそうとしなくて大丈夫です。

私の経験から言うと、fixtureはテストコードを長く育てるための土台です。 最初は小さなfixtureを1つ作るだけでも、テストの読みやすさは変わります。

テスト前の準備で同じコードを何度も書いているなら、次のテストからfixtureを使ってみてください。 その一歩だけで、pytestの便利さがぐっと実感できるはずです。

ここまでお読みいただきありがとうございました。

次のアクション

記事で読んだ内容を、講座で実装してみましょう

Python WebAcademyでは、ブラウザ上でコードを書いて実行結果を確認できます。無料で始められる講座から、学習の流れを試せます。

あわせて読む

関連記事

ブログ一覧へ