Python / LESSON 17 / PY17

Python:pytestによるテスト

1学習の目的

2基礎解説

やりたいこと書き方意味
テストを書くdef test_足し算():関数名を test_ で始める
結果を検証するassert add(3, 7) == 10条件がFalseなら失敗
例外を検証するwith pytest.raises(ValueError):その例外が出れば成功
複数の値で試す@pytest.mark.parametrize(...)同じテストを値違いで繰り返す
✅ 覚えるべき重要ポイント
  1. ファイル名も関数名も test_ で始めるtest_calc.py / def test_足し算():)。pytestが自動で見つけて実行する。
  2. 判定は assert 条件 だけ。失敗すると期待値と実際の値をpytestが表示してくれるので、自分でメッセージを書く必要はない。
  3. 実行はターミナルで pytest と打つだけ。結果は「3 passed」のように件数で表示される。
  4. 1つのテストで1つの観点だけを確かめる。テスト名は「何を確かめるか」を書く(test_端数は切り捨て)。
  5. 特に価値があるのは境界値(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問)

PY17-D01 2つの数を足す関数 add を定義し、assert で add(3, 7) が 10、add(-1, 1) が 0 になることを検証せよ。両方通ったら「テスト成功」と出力すること。 コーディング★☆☆無料
期待される結果

出力 == テスト成功

ヒント

assert 条件 と書くだけ。条件がFalseならその場でエラーになり、下の行は実行されない。

模範解答
def add(a, b):
    return a + b


assert add(3, 7) == 10
assert add(-1, 1) == 0

print("テスト成功")
解説

assert はPythonの標準機能で、pytestが無くても使える。「ここは必ずこうなるはず」を書き残しておくのがテストの出発点。

PY17-D02 pytest が自動的にテストとして実行してくれる関数名はどれか。
A. check_add B. test_add C. add_test D. Test_add
選択★☆☆無料
期待される結果

解答 == B

ヒント

決まった接頭辞で始める必要がある。

模範解答
B
解説

前に付けるのがルール(add_test では見つけてもらえない)。ファイル名も test_calc.py のように test_ で始める。

PY17-D03 assert 1 + 1 == 3 を実行し、発生した例外を受け止めて「AssertionError が発生しました」と出力せよ。 コーディング★☆☆無料
期待される結果

出力 == AssertionError が発生しました

ヒント

try / except AssertionError: で囲む。PY10の例外処理と同じ書き方。

模範解答
try:
    assert 1 + 1 == 3
except AssertionError:
    print("AssertionError が発生しました")
解説

assert が失敗すると AssertionError という例外が出る。pytestはこれを検知して「失敗したテスト」として集計している。

PY17-D04 関数の戻り値を検証するコードを完成させよ。 穴埋め★☆☆無料
コード
def double(n):
    return n * 2


____ double(5) == 10
print("OK")
期待される結果

出力 == OK

ヒント

「〜であることを表明する」という意味の英単語。

模範解答
assert
解説

assert の後ろにはTrue になるはずの条件を書く。== を = と書き間違えると構文エラーになるので注意。

PY17-D05 自動テストを書く最大の利点はどれか。
A. プログラムが速くなる B. コードを変更したとき、他を壊していないか即座に確認できる C. コードの量が減る D. エラーが出なくなる
選択★☆☆無料
期待される結果

解答 == B

ヒント

テストは書いた瞬間より、あとで効いてくる。

模範解答
B
解説

テストの本当の価値は「安心して直せる」こと。テストが無いコードは、怖くて誰も手を入れられなくなっていく。

PY17-D06 点数が60以上なら「合格」、未満なら「不合格」を返す関数 judge と、その境界値を確かめる2つのテスト関数(60点と59点)を test_judge.py に書け。 コーディング★★☆無料
期待される結果

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点のテストで初めて捕まる。

PY17-D07 例外が発生することを検証するコードを完成させよ。 穴埋め★★☆無料
コード
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 ブロックの中で例外が出れば成功、出なければ失敗という逆向きの判定になる。異常系のテストはこの形で書く。

PY17-D08 @pytest.mark.parametrize の役割はどれか。
A. テストを並列実行する B. 同じテストを複数の入力値で繰り返す C. テストの実行順を指定する D. テストをスキップする
選択★★☆無料
期待される結果

解答 == B

ヒント

parameter(引数)を変えながら、が語源。

模範解答
B
解説

1件が1テストとして数えられるので、3パターン指定すれば「3 passed」になる。境界値をまとめて並べるのに最適。

PY17-D09 年齢を受け取り、0未満または150超なら raise ValueError("年齢が不正です")、正常なら "OK" を返す関数 check_age と、正常系1件・異常系2件(-1と200)のテストを書け。 コーディング★★☆無料
期待される結果

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 が、ここで初めて「テストできるもの」として意味を持つ。

PY17-D10 税込計算 with_tax(price, rate=0.1)(整数に切り捨て)について、parametrize を使って (0, 0)、(100, 110)、(1000, 1100)、(999, 1098) の4パターンを1つのテスト関数で検証せよ。 コーディング★★☆無料
期待される結果

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問)

PY17-S01 【税込計算のテスト】with_tax(price, rate=0.1)(整数に切り捨て)に対して、次の4観点のテストを test_tax.py に書け。
① 通常の計算(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円になる。この「切り捨てる」という仕様をテストに書き残すことで、あとから四捨五入に変えたい人が仕様変更に気づける。

PY17-S02 【バリデーション関数のテスト】名前とパスワードを受け取り、名前が空なら ValueError、パスワードが8文字未満なら ValueError、問題なければ "OK" を返す validate 関数と、次の4つのテストを書け。
① 正常な入力は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文字が弾かれるが、そのバグは④のテストでしか捕まらない。

PY17-S03 【別ファイルの関数をテストする】同じフォルダに calc.py があり、add(a, b)divide(a, b)(b が0なら ValueError)が定義されている。この2関数に対する test_calc.py を書け(足し算・割り算・ゼロ除算の3観点)。 コーディング★★☆無料
期待される結果

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のモジュールがそのまま活きる。

PY17-S04 【境界値をまとめてテスト】点数が80以上なら「A」、60以上なら「B」、それ未満なら「C」を返す judge 関数を作り、parametrize で (100,"A") (80,"A") (79,"B") (60,"B") (59,"C") (0,"C") の6パターンを1つのテスト関数で検証せよ。 コーディング★★★無料
期待される結果

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で学んだ「条件は厳しい順に」が守られているかも、このテストが保証してくれる。

PY17-S05 【再発防止テスト】割引計算 final_price(price, is_member=False, coupon=False) に「クーポンを使うと支払額がマイナスになる」というバグが見つかった。マイナスなら0にする修正を加えたうえで、次の4つのテストを書け。
① 会員は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仕上げ課題

PY17-FINAL 【請求書計算のテストスイート】同じフォルダに billing.py があり、次の4つの関数が定義されている。これに対する test_billing.py を書け。

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 と打つだけで壊れていないと確認できる。「怖くて触れないコード」から「安心して直せるコード」に変わった——これがテストを書く理由のすべて。