Python:pytestによるテスト
1学習の目的
- assert と pytest でテストを書き、コードが期待どおり動くかを自動で確認できるようになる。手作業で毎回動かして目視確認する必要がなくなる。
- 例外のテストや境界値のテストを書けるようになる。コードを直すたびに「他を壊していないか」を数秒で確認できる状態を作れる。
2基礎解説
| やりたいこと | 書き方 | 意味 |
|---|---|---|
| テストを書く | def test_足し算(): | 関数名を test_ で始める |
| 結果を検証する | assert add(3, 7) == 10 | 条件がFalseなら失敗 |
| 例外を検証する | with pytest.raises(ValueError): | その例外が出れば成功 |
| 複数の値で試す | @pytest.mark.parametrize(...) | 同じテストを値違いで繰り返す |
- ファイル名も関数名も test_ で始める(test_calc.py / def test_足し算():)。pytestが自動で見つけて実行する。
- 判定は assert 条件 だけ。失敗すると期待値と実際の値をpytestが表示してくれるので、自分でメッセージを書く必要はない。
- 実行はターミナルで pytest と打つだけ。結果は「3 passed」のように件数で表示される。
- 1つのテストで1つの観点だけを確かめる。テスト名は「何を確かめるか」を書く(test_端数は切り捨て)。
- 特に価値があるのは境界値(0、ちょうど基準値、その1つ下)と異常値(マイナス、空、上限超え)のテスト。バグはほぼそこに出る。
現場使用例:リファクタリング前に現状の動作をテストで固定する、バグを直したら同じバグの再発防止テストを追加する、GitHubにpushするたびCIで自動実行する。pip install pytest が必要な外部ライブラリ。
# test_calc.py として保存し、ターミナルで pytest と実行する
import pytest
def with_tax(price, rate=0.1):
return int(price * (1 + rate))
def test_通常の税込計算():
assert with_tax(1000) == 1100
def test_端数は切り捨て():
assert with_tax(105) == 115 # 115.5 → 115
@pytest.mark.parametrize("price, expected", [(0, 0), (100, 110), (999, 1098)])
def test_複数パターン(price, expected):
assert with_tax(price) == expected
# $ pytest → 5 passed
3基本ドリル(10問)
出力 == テスト成功
assert 条件 と書くだけ。条件がFalseならその場でエラーになり、下の行は実行されない。
def add(a, b):
return a + b
assert add(3, 7) == 10
assert add(-1, 1) == 0
print("テスト成功")
assert はPythonの標準機能で、pytestが無くても使える。「ここは必ずこうなるはず」を書き残しておくのがテストの出発点。
A. check_add B. test_add C. add_test D. Test_add 選択★☆☆無料
解答 == B
決まった接頭辞で始める必要がある。
B
前に付けるのがルール(add_test では見つけてもらえない)。ファイル名も test_calc.py のように test_ で始める。
出力 == AssertionError が発生しました
try / except AssertionError: で囲む。PY10の例外処理と同じ書き方。
try:
assert 1 + 1 == 3
except AssertionError:
print("AssertionError が発生しました")
assert が失敗すると AssertionError という例外が出る。pytestはこれを検知して「失敗したテスト」として集計している。
def double(n):
return n * 2
____ double(5) == 10
print("OK")
出力 == OK
「〜であることを表明する」という意味の英単語。
assert
assert の後ろにはTrue になるはずの条件を書く。== を = と書き間違えると構文エラーになるので注意。
A. プログラムが速くなる B. コードを変更したとき、他を壊していないか即座に確認できる C. コードの量が減る D. エラーが出なくなる 選択★☆☆無料
解答 == B
テストは書いた瞬間より、あとで効いてくる。
B
テストの本当の価値は「安心して直せる」こと。テストが無いコードは、怖くて誰も手を入れられなくなっていく。
pytest 実行で 2 passed
ファイル名を test_judge.py にして、def test_〜(): の中で assert する。実行はターミナルで pytest。
def judge(score):
if score >= 60:
return "合格"
return "不合格"
def test_60点はちょうど合格():
assert judge(60) == "合格"
def test_59点は不合格():
assert judge(59) == "不合格"
境界の両側を必ず両方テストする。>= を > と書き間違えるバグは、59点のテストだけでは見つからず60点のテストで初めて捕まる。
import pytest
def divide(a, b):
if b == 0:
raise ValueError("0では割れません")
return a / b
def test_ゼロ除算で例外():
with pytest.____(ValueError):
divide(10, 0)
pytest 実行で 1 passed
「(例外が)発生する」という意味のメソッド。
raises
with ブロックの中で例外が出れば成功、出なければ失敗という逆向きの判定になる。異常系のテストはこの形で書く。
A. テストを並列実行する B. 同じテストを複数の入力値で繰り返す C. テストの実行順を指定する D. テストをスキップする 選択★★☆無料
解答 == B
parameter(引数)を変えながら、が語源。
B
1件が1テストとして数えられるので、3パターン指定すれば「3 passed」になる。境界値をまとめて並べるのに最適。
pytest 実行で 3 passed
異常系は with pytest.raises(ValueError): の中で呼ぶ。import pytest を忘れずに。
import pytest
def check_age(age):
if age < 0 or age > 150:
raise ValueError("年齢が不正です")
return "OK"
def test_正常な年齢():
assert check_age(30) == "OK"
def test_マイナスは例外():
with pytest.raises(ValueError):
check_age(-1)
def test_上限超えは例外():
with pytest.raises(ValueError):
check_age(200)
正常系と異常系をセットで書くのが基本。PY10で書いた raise が、ここで初めて「テストできるもの」として意味を持つ。
pytest 実行で 4 passed
@pytest.mark.parametrize("price, expected", [(0, 0), ...]) を付け、関数の引数に price と expected を受け取る。
import pytest
def with_tax(price, rate=0.1):
return int(price * (1 + rate))
@pytest.mark.parametrize("price, expected", [(0, 0), (100, 110), (1000, 1100), (999, 1098)])
def test_税込計算(price, expected):
assert with_tax(price) == expected
同じ assert を4回コピペする代わりに1つで済む。パターンを1行足すだけでテストが増やせるので、あとから気づいた条件も気軽に追加できる。
4実践シナリオ(5問)
① 通常の計算(1,000円 → 1,100円)② 端数の切り捨て(105円 → 115円)③ 0円のとき0円 ④ 税率を8%と指定したとき(1,000円 → 1,080円) コーディング★★☆無料
pytest 実行で 4 passed
テスト名で観点が分かるようにする(test_端数は切り捨て など)。④は with_tax(1000, 0.08) のように第2引数を渡す。
def with_tax(price, rate=0.1):
return int(price * (1 + rate))
def test_通常の税込計算():
assert with_tax(1000) == 1100
def test_端数は切り捨て():
assert with_tax(105) == 115
def test_ゼロ円はゼロ円():
assert with_tax(0) == 0
def test_税率を指定できる():
assert with_tax(1000, 0.08) == 1080
105円の税込は115.5円だが int() で115円になる。この「切り捨てる」という仕様をテストに書き残すことで、あとから四捨五入に変えたい人が仕様変更に気づける。
① 正常な入力はOK ② 名前が空なら例外 ③ パスワードが短いと例外 ④ ちょうど8文字ならOK コーディング★★☆無料
pytest 実行で 4 passed
④が境界値のテスト。8文字ちょうどは通るのが正しい仕様なので、例外にならないことを確かめる。
import pytest
def validate(name, password):
if not name:
raise ValueError("名前を入力してください")
if len(password) < 8:
raise ValueError("パスワードは8文字以上にしてください")
return "OK"
def test_正常な入力はOK():
assert validate("田中", "abcd1234") == "OK"
def test_名前が空なら例外():
with pytest.raises(ValueError):
validate("", "abcd1234")
def test_パスワードが短いと例外():
with pytest.raises(ValueError):
validate("田中", "abc123")
def test_ちょうど8文字はOK():
assert validate("田中", "abcd1234") == "OK"
「8文字以上」の境界は7文字と8文字。< を <= と書き間違えると8文字が弾かれるが、そのバグは④のテストでしか捕まらない。
pytest 実行で 3 passed
テストファイルの先頭で from calc import add, divide と書けば、別ファイルの関数を呼べる(PY12のモジュール)。
import pytest
from calc import add, divide
def test_足し算():
assert add(3, 7) == 10
def test_割り算():
assert divide(10, 2) == 5
def test_ゼロ除算は例外():
with pytest.raises(ValueError):
divide(10, 0)
本体とテストはファイルを分けるのが実務の形。calc.py は納品するコード、test_calc.py は開発用。import で結ぶだけなので、PY12のモジュールがそのまま活きる。
pytest 実行で 6 passed
各しきい値の「ちょうど」と「1つ下」をペアで並べるのがコツ。80と79、60と59が対になっている。
import pytest
def judge(score):
if score >= 80:
return "A"
if score >= 60:
return "B"
return "C"
@pytest.mark.parametrize("score, expected", [
(100, "A"),
(80, "A"),
(79, "B"),
(60, "B"),
(59, "C"),
(0, "C"),
])
def test_ランク判定(score, expected):
assert judge(score) == expected
しきい値ごとに「ちょうど」と「1つ下」を並べる——これだけで境界のバグはほぼ全部捕まる。PY04で学んだ「条件は厳しい順に」が守られているかも、このテストが保証してくれる。
① 会員は10%引き(12,000→10,800)② クーポンで500円引き(1,000→500)③ マイナスにならない(400+クーポン→0) ④ 何もなしなら定価のまま コーディング★★★無料
pytest 実行で 4 passed
③がこのバグの再発防止テスト。テスト名にその意図を書き残すと、あとから読む人に理由が伝わる。
def final_price(price, is_member=False, coupon=False):
if is_member:
price = int(price * 0.9)
if coupon:
price -= 500
if price < 0:
price = 0
return price
def test_会員は10パーセント引き():
assert final_price(12000, is_member=True) == 10800
def test_クーポンで500円引き():
assert final_price(1000, coupon=True) == 500
def test_支払額はマイナスにならない():
assert final_price(400, coupon=True) == 0
def test_割引なしなら定価():
assert final_price(1000) == 1000
バグを直したら、そのバグを再現するテストを必ず追加する。これを回帰テストと呼ぶ。将来この行を誰かが消してしまっても、テストが即座に知らせてくれる。テストが最も価値を発揮する場面。
5仕上げ課題
subtotal(price, qty) … 単価×数量
apply_discount(amount, is_member) … 会員なら10%引き(整数に切り捨て)、そうでなければそのまま
with_tax(amount, rate=0.1) … 税込(整数に切り捨て)
invoice_total(items, is_member) … 明細 [(商品名, 単価, 数量), ...] から小計→割引→税込を通した最終請求額
次の観点をすべてテストすること(合計11件が通る構成):
① 小計の計算 ② 数量0なら小計0
③ 会員は10%引き ④ 非会員は割引なし
⑤ 税込の計算 ⑥ 税率を指定できる
⑦ 税込計算を parametrize で3パターン(0円→0円 / 100円→110円 / 999円→1,098円)
⑧ 明細3件の請求書合計が 137,511円
[("ノートPC", 128000, 1), ("マウス", 3200, 2), ("USBメモリ", 1500, 3)] / 会員
⑨ 空の明細なら0円
期待結果:pytest 実行で 11 passed
pytest 実行で 11 passed
from billing import subtotal, apply_discount, with_tax, invoice_total で読み込む。⑦は parametrize で3件なので3テストとして数えられ、他の8観点と合わせて11件になる。⑨のような「空っぽのとき」は忘れられがちだがバグの温床なので必ず入れる。
import pytest
from billing import apply_discount, invoice_total, subtotal, with_tax
def test_小計は単価かける数量():
assert subtotal(1500, 3) == 4500
def test_数量ゼロなら小計ゼロ():
assert subtotal(1500, 0) == 0
def test_会員は10パーセント引き():
assert apply_discount(10000, True) == 9000
def test_非会員は割引なし():
assert apply_discount(10000, False) == 10000
def test_税込計算():
assert with_tax(1000) == 1100
def test_税率を指定できる():
assert with_tax(1000, 0.08) == 1080
@pytest.mark.parametrize("amount, expected", [(0, 0), (100, 110), (999, 1098)])
def test_税込計算の複数パターン(amount, expected):
assert with_tax(amount) == expected
def test_請求書の合計():
items = [
("ノートPC", 128000, 1),
("マウス", 3200, 2),
("USBメモリ", 1500, 3),
]
assert invoice_total(items, True) == 137511
def test_空の明細はゼロ円():
assert invoice_total([], False) == 0
部品ごとのテスト(①〜⑦)と、組み合わせた全体のテスト(⑧⑨)の両方があるのがよいテストスイート。部品のテストは壊れた場所を特定してくれ、全体のテストは繋ぎ方の間違いを見つけてくれる。
PY08から何度も書き直してきたこの請求書計算に、ようやくテストが付いた。これで税率を変えても、割引ロジックを直しても、pytest と打つだけで壊れていないと確認できる。「怖くて触れないコード」から「安心して直せるコード」に変わった——これがテストを書く理由のすべて。