単体テストって何?まずはゴールのイメージから
単体テスト(ユニットテスト)は、
「プログラムを小さな“部品(ユニット)”ごとに分けて、その部品がちゃんと動くかを確認するテスト」です。
ここでいう“部品”は、主に関数やメソッドです。
「この関数は、こういう入力を渡したら、こういう結果を返すべき」という約束を、コードで確認するのが単体テストです。
重要なのは、「小さく」「他から切り離して」テストすることです。
DB や外部 API などに依存しないようにして、その関数のロジックだけを純粋にチェックするのが理想の単体テストです。
いちばんシンプルな単体テストの例
テスト対象の関数を用意する
まずは、シンプルな料金計算関数を例にします。
# price.py
def calc_price(unit_price: int, quantity: int) -> int:
if quantity < 0:
raise ValueError("quantity must be >= 0")
return unit_price * quantity
Pythonやっていることは単純ですが、
「正常系」と「異常系(エラー)」の両方をテストできます。
pytest を使った単体テスト
この関数に対する単体テストを test_price.py に書きます。
# test_price.py
import pytest
from price import calc_price
def test_calc_price_normal():
assert calc_price(100, 3) == 300
def test_calc_price_zero_quantity():
assert calc_price(100, 0) == 0
def test_calc_price_negative_quantity():
with pytest.raises(ValueError):
calc_price(100, -1)
Pythonここでやっていることは、とても単純です。
test_ で始まる関数をテストとして定義するassert で「期待する結果」を書く
エラーになるべきケースは pytest.raises で確認する
これだけで、もう立派な単体テストです。
単体テストの「単体」とは何かをちゃんと理解する
「その関数だけ」をテストする、という考え方
単体テストの“単体”は、「できるだけ小さい単位」を意味します。
典型的には、次のようなイメージです。
一つの関数
一つのメソッド
一つのクラスの一つの振る舞い
ここで大事なのは、「そのユニットの責務だけをテストする」ということです。
例えば、calc_price の単体テストでは、
DB に保存するとか、ログを出すとか、そういうことは一切関係ありません。
「引数に応じて正しい値を返すか」「不正な引数でエラーを出すか」だけを見ます。
外部依存を切り離す、という超重要ポイント
単体テストをきれいに書くためには、
「外部に依存しないように設計する」ことがとても重要です。
外部 API を呼ぶ
DB にアクセスする
ファイルを読む・書く
現在時刻や乱数に依存する
こういったものは、単体テストではできるだけ避けます。
どうしても必要な場合は、mock や monkeypatch で「ニセモノ」に差し替えて、
その関数のロジックだけをテストできるようにします。
単体テストが書きにくいコードは、たいてい「責務が混ざりすぎている」コードです。
逆に、単体テストが書きやすいコードは、自然と設計もきれいになります。
単体テストで押さえておきたい観点
正常系だけでなく「境界」と「異常系」もテストする
単体テストでは、次のような観点を意識すると質が上がります。
普通のケース(よくある入力)
境界のケース(0、空文字、最大値など)
異常なケース(不正な入力、エラーになるべき状況)
さっきの calc_price でいうと、
普通のケース:unit_price=100, quantity=3
境界のケース:quantity=0
異常なケース:quantity<0
をそれぞれ別のテストとして書きました。
これを意識しておくと、「動いているように見えるけど、端っこでバグる」パターンをかなり防げます。
1 テスト関数=1 つの観点
単体テストでは、テスト関数を「観点ごと」に分けるのが基本です。
test_calc_price_normaltest_calc_price_zero_quantitytest_calc_price_negative_quantity
のように、「何を確認しているテストか」が名前から分かるようにします。
1 つのテスト関数にいろいろ詰め込みすぎると、
どこで失敗したのか分かりにくくなります。
「このテストは何を保証しているのか」を、テスト名で説明できるように意識すると良いです。
単体テストと設計の関係(ここが本当に大事)
「テストしやすいコード」は「設計が良いコード」
単体テストを書こうとして、こう感じたことはありませんか?
この関数、テストしようとすると DB が必要になる
このメソッド、外部 API を叩かないと動かない
現在時刻に依存していて、結果が毎回変わる
こういうとき、「テストが面倒だな」で終わらせずに、
「そもそも設計を分けた方がいいのでは?」と考えるのが、上達のポイントです。
例えば、料金計算と DB 保存が一緒になっている関数があったとします。
def create_order_and_save(db, unit_price, quantity):
if quantity < 0:
raise ValueError("quantity must be >= 0")
total = unit_price * quantity
db.insert_order(unit_price, quantity, total)
return total
Pythonこれを単体テストしようとすると、DB をどうするかで悩みます。
本当は、こう分けた方がテストしやすくなります。
def calc_price(unit_price, quantity):
if quantity < 0:
raise ValueError("quantity must be >= 0")
return unit_price * quantity
def create_order_and_save(db, unit_price, quantity):
total = calc_price(unit_price, quantity)
db.insert_order(unit_price, quantity, total)
return total
Pythonこうすると、
calc_price は純粋な単体テストが書けるcreate_order_and_save は、DB を mock にして「呼び出し方」をテストできる
という構造になります。
単体テストを書こうとすること自体が、
「責務を分ける」「ロジックを純粋にする」ためのガイドになってくれます。
単体テストは「バグ検出」だけじゃなく「リファクタリングの保険」
単体テストがあると、
コードを安心して書き換えられるようになります。
内部の実装を変えても、
単体テストが全部通っていれば「外から見た振る舞いは変わっていない」と言えます。
逆に、単体テストがないと、
ちょっと直したつもりが別のところを壊す
怖くて大きな変更ができない
「動いているから触りたくない」コードが増える
という状態になりがちです。
単体テストは、「未来の自分を守るための保険」です。
今の自分が書いた仕様を、テストという形で残しておくことで、
数ヶ月後の自分が安心してリファクタリングできるようになります。
単体テストを始めるときの現実的なステップ
いきなり全部は無理でいい。「ここだけは壊したくない」から始める
既存コードがある場合、
「全部に単体テストを書く」のは現実的ではありません。
最初は、こういうところから始めるのがおすすめです。
お金や数量など、計算ロジックが絡むところ
バリデーション(入力チェック)
文字列整形(フォーマット)
日付や期間の計算
こういうところは、バグると地味に痛いし、
単体テストも書きやすいです。
まずは 1 ファイル、1 関数、1 テストからで十分です。
そこから少しずつ、「テストがある範囲」を広げていけばいいです。
新しいコードには、最初から単体テストをセットで書く
既存コードは「後追い」で少しずつ。
新しく書くコードは、「最初から単体テストとセットで」書く。
この習慣がつくと、
自然と「テストしやすい設計」を意識するようになります。
テストを書きにくいコードを書いてしまったら、
「これ、責務が混ざってないか?」と自分にツッコミを入れる。
その繰り返しが、設計の筋トレになります。
まとめ(単体テストは「小さく・切り離して・守る」ための仕組み)
単体テストを初心者目線で整理すると、こうなります。
関数やメソッドなどの「小さな部品」を、他から切り離してテストするのが単体テスト。
正常系だけでなく、境界値や異常系も含めて「その関数の仕様」をテストで表現する。
外部依存(DB、API、ファイル、時刻など)を切り離すことで、単体テストが書きやすくなり、その過程で設計も良くなる。
単体テストはバグ検出だけでなく、「リファクタリングの保険」として、未来の自分を守ってくれる。
