1. 単体テスト(Unit Test)の基本
まずはシンプルな関数を用意して、偶数かどうかを判定します。
math_utils.py
def is_even(n: int) -> bool:
return n % 2 == 0
test_math_utils.py
from math_utils import is_even
def test_is_even():
assert is_even(2)
assert not is_even(3)
assert is_even(100)
-
def test_is_even():
→ テスト関数を定義しています。先頭にtestと付けることでpytestが、テストと認識します。 -
assert is_even(2)
→is_even(2)がTrueを返すことを確認します。期待どおりならOK。 -
assert not is_even(3)
→is_even(3)がFalseを返すことを確認します。notをつけているので、「False を期待している」ことになります。 -
assert is_even(100)
→ 100 も偶数なので、Trueになるべきです。
ターミナルで以下のコマンドを実行:
pytest test_math_utils.py
成功すればこんな感じで表示されます:
============================= test session starts =============================
collected 1 item
test_math_utils.py . [100%]
============================== 1 passed in 0.01s ==============================
2. パラメータ化されたテスト
import pytest
from math_utils import is_even
@pytest.mark.parametrize(("n", "expected"), [
(1, False),
(2, True),
(3, False),
(10, True),
])
def test_is_even(n, expected):
assert is_even(n) == expected
同じ関数に対して、いろんな入力と結果をまとめてテストしたい時は @pytest.mark.parametrize を使うと便利です。
@pytest.mark.parametrize(("n", "expected"), [
(1, False),
(2, True),
(3, False),
(10, True),
])
この部分で 「テストデータの一覧」 を定義しています。
-
("n", "expected")
→ これは各テストに渡される引数の名前です。 -
[(1, False), (2, True), ...]
→ それぞれのタプルが「テストケース」です。
たとえば(1, False)は「is_even(1)の結果はFalseであるべき」という意味。
def test_is_even(n, expected):
assert is_even(n) == expected
-
nとexpectedが、上のparametrizeのリストから自動で渡されます。 -
is_even(n)の結果がexpectedと一致しているかをチェックします。
実際には以下のように 書くコードは1つなのに4つのテスト が実行されます:
assert is_even(1) == Falseassert is_even(2) == Trueassert is_even(3) == Falseassert is_even(10) == True
3. フィクスチャ(前処理・後処理)
「テストに必要な準備(前処理)」や「クリーンアップ(後処理)」を行いたい時は @pytest.fixture を使います。
下記の例では、ファイルの中身を読み取って、その中身が期待通りの数値かどうかを比較する」 テストを行います。
ファイルの読み込み関数:
# file_utils.py
from typing import List
def read_numbers(path: str) -> List[int]:
with open(path) as f:
return [int(line.strip()) for line in f.readlines()]
テスト+フィクスチャ:
# test_file_utils.py
import pytest
from file_utils import read_numbers
@pytest.fixture
def numbers_file(tmp_path) -> str:
file = tmp_path / "numbers.txt"
file.write_text("3\n1\n2\n")
return str(file)
def test_read_numbers(numbers_file):
assert sorted(read_numbers(numbers_file)) == [1, 2, 3]
| 行 | 説明 |
|---|---|
@pytest.fixture |
フィクスチャの定義。テストの前に呼ばれる準備関数です。 |
tmp_path |
pytest が自動で用意してくれる「一時的な空フォルダ」。テストが終われば自動で削除されます。 |
file = tmp_path / "numbers.txt" |
その中に "numbers.txt" というファイルを作る準備 |
file.write_text("3\n1\n2\n") |
ファイルの中身として「3 改行 1 改行 2」を書き込む |
return str(file) |
ファイルのパスを文字列として返す |
@pytest.fixtureを使う事で、
- テストの中で直接ファイル操作しなくていい
- 複数のテストで 使い回し できる
- テスト後に 自動で片付けてくれる(tmp_path)
- データベースの初期化・クラスのセットアップなどにも使える
4. モック(外部の処理を偽装する)
下記の状況の場合、pytest-mock を使ってモック化することで実現することができます。
- 外部APIの呼び出しをテストしたいとき
- データベース操作など、本当に実行したくない処理を偽装したいとき
- 関数が呼ばれたか・何回呼ばれたか・どんな引数で呼ばれたかをチェックしたいとき
本体コード:
# messenger.py
def send_message(msg: str):
log_message(msg)
def log_message(msg: str):
print(f"[LOG] {msg}")
目的としては、
- 関数
send_message()の中でlog_message()を呼んでいるけど、本当にlog_message("Test")が呼ばれたか?を確認したい - でも実際に
print()されたくないので モック(偽の関数) にすり替えます
テストコード:
def test_send_message(mocker):
log_mock = mocker.patch("messenger.log_message")
from messenger import send_message
send_message("Test")
log_mock.assert_called_once_with("Test")
① mocker.patch("messenger.log_message")
-
messenger.log_messageを モック(偽の関数)に差し替え ます。 - つまり
print()は実行されず、呼び出し記録だけが残る。 -
log_mockというモックオブジェクトが返ってくる。
② from messenger import send_message
- patch の後でインポートすることが大事
- なぜなら、
send_messageの中で呼ばれるlog_messageがすでにモックに置き換わっている必要があるから。
③ send_message("Test")
- これで
log_message("Test")が呼ばれる(モックになってる)
④ log_mock.assert_called_once_with("Test")
- 「1回だけ」「引数が
"Test"だった」という条件で呼ばれたかチェック - これが間違っているとテストは失敗します
まとめ
Pytestを使えば、シンプルな関数からファイル操作、標準出力、外部依存の検証まで、さまざまなテストが簡単に書けます。
上手に活用する事で安全安心に開発を進めていくことができます。