〜「動いてるからテストコードはいらない」と思っていたら、半年後の自分に泣かされた話〜
こんにちは!ハンズオンラボ運営のわたるです。
「手元で動作確認したから大丈夫」――そう思って書いたコードが、数ヶ月後に別の修正のせいで壊れていたことに気づかなかった経験はありませんか?
この記事では、こうした事故を防ぐ単体テスト(ユニットテスト)の考え方を、「出荷前の部品検査」 というたとえで解説します。
この記事を読むと、以下のことができるようになります
- 単体テストが何を目的とした仕組みなのか説明できる
- 実際にPythonでテストコードを書いて実行できる
- 「テストがあると何が嬉しいのか」を具体的な場面で説明できる
単体テストとは何か——「出荷前の部品検査」
自動車工場で、エンジンやブレーキといった部品1つ1つを車に組み込む前に、それぞれ単体で検査する工程があります。「このブレーキ部品は、規定の圧力で確実に止まるか」を、車全体に取り付ける前に確認するのです。
もしこの検査を省略して、車が完成してから初めて「ブレーキが効かない」と気づいたらどうなるでしょうか。どの部品が原因か特定するだけでも一苦労ですし、最悪の場合は事故につながります。
プログラミングにおける単体テストは、この「部品検査」と同じ役割を果たします。関数やクラスといった小さな部品が、期待通りに動作するかを、システム全体に組み込む前に個別に確認する仕組みです。
- 部品(エンジン・ブレーキ) → 関数・クラス
- 検査基準(規定の圧力で止まるか) → テストコードに書かれた「期待する結果」
- 検査に合格した部品だけを車に搭載 → テストが通ったコードだけをマージする
実際にテストコードを書いてみる
「税込価格を計算する関数」を例に、単体テストを体験してみます。
# calculator.py
def calculate_tax_included_price(price: int, tax_rate: float = 0.1) -> int:
"""税込価格を計算する(1円未満切り捨て)"""
return int(price * (1 + tax_rate))
この関数が正しく動くかを確認するテストコードを、Pythonの標準的なテストフレームワークpytestを使って書きます。
# test_calculator.py
from calculator import calculate_tax_included_price
def test_calculate_tax_included_price_normal():
# 1000円の商品は税込1100円になるはず
assert calculate_tax_included_price(1000) == 1100
def test_calculate_tax_included_price_rounding():
# 999円 × 1.1 = 1098.9 → 1円未満切り捨てで1098円になるはず
assert calculate_tax_included_price(999) == 1098
def test_calculate_tax_included_price_zero():
# 0円の商品は税込でも0円のはず
assert calculate_tax_included_price(0) == 0
# テストを実行する
pytest test_calculator.py -v
実行結果:
test_calculator.py::test_calculate_tax_included_price_normal PASSED
test_calculator.py::test_calculate_tax_included_price_rounding PASSED
test_calculator.py::test_calculate_tax_included_price_zero PASSED
「1000円なら1100円になるはず」という期待値をコードとして残しておくことで、この関数が正しく動いているかを何度でも自動で再確認できます。
なぜ「動作確認した」だけでは不十分なのか
工場の部品検査は、一度合格したら終わりではありません。新しいロットが出荷されるたびに、毎回同じ検査を繰り返します。プログラムも同じで、「一度動作確認した」という事実は、次にコードが変更された瞬間に無効になります。
単体テストの最大の価値は、**「過去に書いたテストを、今後何度でも自動で再実行できる」ことです。半年後に別の担当者がコードを修正しても、テストコードさえ残っていれば、pytestを実行するだけで「壊れていないか」を数秒で確認できます。これをリグレッションテスト(デグレ検知)**と呼びます。
良いテストコードの書き方:正常系・異常系・境界値
部品検査では、「普通に使ったとき」だけでなく「限界まで使ったとき」「間違った使い方をしたとき」もチェックします。テストコードも同様に、3つの観点を意識すると漏れが少なくなります。
| 観点 | 内容 | 今回の例 |
|---|---|---|
| 正常系 | 想定通りの入力で正しく動くか | 1000円→1100円 |
| 異常系 | 不正な入力に対して適切に処理されるか | マイナスの価格を渡したらエラーになるか |
| 境界値 | 計算の境目でおかしな挙動をしないか | 0円、小数点以下の切り捨てが発生する金額 |
def test_calculate_tax_included_price_negative():
# マイナスの価格が渡された場合は例外を発生させたい
import pytest
with pytest.raises(ValueError):
calculate_tax_included_price(-100)
「正常に動く場合」だけをテストしていると、想定外の入力が来たときの挙動が保証されないままになります。異常系・境界値までカバーしておくことで、部品検査としての信頼性が格段に上がります。
テストカバレッジ:どれだけ検査が行き届いているか
工場の検査で「今月出荷した部品のうち、何%を検査済みか」を管理するように、プログラムにもテストカバレッジという指標があります。これは「コード全体のうち、テストによってチェックされている割合」を数値化したものです。
# pytestとカバレッジ計測ツールを組み合わせて実行する例
pytest --cov=calculator test_calculator.py
実行結果(一部抜粋):
Name Stmts Miss Cover
-----------------------------------
calculator.py 8 1 88%
「カバレッジ100%を目指すべき」というわけではありませんが、重要な処理ほどカバレッジが低い状態は危険信号です。お金の計算や認証処理など、間違えると影響が大きい部品ほど、優先的に検査(テスト)の網を広げておくのが実務での考え方です。
よくある勘違い:「テストを書く時間がもったいない」
「テストコードを書く時間があったら、機能を1つでも多く実装したい」と感じることもあるかもしれません。しかし実際には、手動での動作確認を毎回繰り返す時間と、テストコードを書いて自動化する時間を比較すると、修正が発生するたびに手動確認を繰り返すコストの方が、長期的には大きくなりがちです。
工場の検査も、「毎回人間が目視で確認する」より「検査機械(自動テスト)に任せる」方が、時間もコストも抑えられ、かつ見落としも減ります。テストコードは「今すぐの手間」ではなく、「将来の修正を安全に行うための投資」と捉えるのが実務での基本姿勢です。
ビフォーアフター
【ビフォー】
- テスト → 「手動で1回動かして確認すれば十分」
- テストコード → 「時間の無駄、機能実装を優先すべき」
- バグ → 「修正のたびに手動で全部確認し直す」
【アフター】
- テスト → 「出荷前の部品検査。何度でも自動で再確認できる仕組み」
- テストコード → 「将来の修正を安全に行うための投資」
- バグ → 「テストを実行するだけで、壊れた部品(関数)を自動で特定できる」
まとめ
この記事では、単体テストの考え方を整理しました。
- 単体テストは「関数・クラスという部品」を、組み込む前に個別検査する仕組み
- 一度書いたテストは、コードが変更されるたびに何度でも自動で再実行できる
- 「動作確認した」という事実は、コードが変更された瞬間に無効になる
- 正常系・異常系・境界値の3つの観点でテストを書くと漏れが少なくなる
- テストコードは「今の手間」ではなく「将来の修正を安全にする投資」
今書いている関数に、1つでもテストコードを追加してみてください。最初は「正常系を1つ書くだけ」で構いません。その1つが、半年後のあなた自身を助けてくれるはずです。
ハンズオンラボでは、未経験からでも「作って覚える」をモットーにしたITハンズオンイベントを定期開催しています。
面白かったら
「👇いいね」で応援

