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) == False
assert is_even(2) == True
assert is_even(3) == False
assert 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を使えば、シンプルな関数からファイル操作、標準出力、外部依存の検証まで、さまざまなテストが簡単に書けます。
上手に活用する事で安全安心に開発を進めていくことができます。