モックとスタブでテストする
テストダブルとは、本物の代わりにテストで使う偽物の部品の総称で、スタブやモックがその仲間です。時計や乱数のように毎回変わるものに頼る関数も、偽物を使えば毎回同じ結果でテストできます。
朝に動かすと合格し、夜に動かすと落ちるテストは、信じられません。偽物は、それを防ぎます。
このレッスンのゴール
メモ
時刻や乱数に頼る関数を、偽物に替えてテストすることができる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| 依存 | 関数が頼っている、外の部品 | 時計、乱数 |
| テストダブル | テストで本物の代わりに使う偽物の総称 | |
| スタブ | 決めた値を返すだけの偽物 | |
| モック | 呼ばれ方を記録する偽物 | |
| 注入 | 偽物を引数として、関数に渡すこと |
学生が入力するセルは、このプレビューでは空白です。
毎回変わる値に頼るとテストが揺れる
挨拶の関数が、いまの時刻を時計から読むとします。朝に動かすと「おはよう」、夜に動かすと「こんばんは」が出ます。
同じテストが、動かす時刻で合格したり落ちたりします。乱数も同じです。
期待値を決められないテストは、役に立ちません。
例: 頼る部品と偽物
| 頼る部品 | 毎回変わる理由 | 偽物に替えると |
|---|---|---|
| 時計 | 動かす時刻が毎回違う | 決めた時刻を返す |
| 乱数 | 実行ごとに違う数が出る | 決めた数を返す |
| ネットワーク | 遅い、つながらないことがある | 決めた応答を返す |
学生が入力するセルは、このプレビューでは空白です。
決めた時刻を返す偽物
def greeting(get_hour):
hour = get_hour()
if hour < 12:
return "おはようございます"
if hour < 18:
return "こんにちは"
return "こんばんは"
# 本物の時計の代わりに、決めた時刻を返す偽物を作ります。
def fake_morning():
return 9
print(greeting(fake_morning))
# fake_morning が 20 を返すように変えて、もう一度実行しましょう。
偽物は引数で渡す
時計を関数の中で直接読むと、替えられません。時計を読む関数を、引数として受け取るようにします。これが注入です。
本番では本物の時計を渡し、テストでは決めた時刻を返す偽物を渡します。関数の中身は、1行も変わりません。
作業用のフォルダを作る
mkdir -p ~/testing/fakesここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
そのフォルダへ移る
cd ~/testing/fakesここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
時計を受け取る関数を書く
printf 'def greeting(get_hour):\n hour = get_hour()\n if hour < 12:\n return "good morning"\n if hour < 18:\n return "good afternoon"\n return "good evening"\n' > greet.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
3つの偽物でテストを書く
printf 'import unittest\nfrom greet import greeting\n\n\ndef fake_nine():\n return 9\n\n\ndef fake_noon():\n return 12\n\n\ndef fake_night():\n return 20\n\n\nclass TestGreeting(unittest.TestCase):\n def test_morning(self):\n self.assertEqual(greeting(fake_nine), "good morning")\n\n def test_noon(self):\n self.assertEqual(greeting(fake_noon), "good afternoon")\n\n def test_night(self):\n self.assertEqual(greeting(fake_night), "good evening")\n' > test_greet.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
テストを動かす
python3 -m unittest 2>&1 | tail -n 1ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
いつ動かしても OK になる
最後の行は OK です。この結果は、朝に動かしても夜に動かしても変わりません。
偽物は、9時、12時、20時の3つを用意しました。12時は、午前と午後の境目です。前のレッスンで学んだ境界値を、ここでも使っています。
ラボで作るファイルは、英語の文字だけで書いています。
例: スタブとモックの違い
| 偽物 | することは | 確かめること |
|---|---|---|
| スタブ | 決めた値を返す | 関数の結果 |
| モック | 呼ばれ方を記録する | 何回、どの引数で呼ばれたか |
学生が入力するセルは、このプレビューでは空白です。
呼ばれ方を記録する偽物
calls = []
def pick_menu(menus, roll):
if len(menus) == 0:
return ""
return menus[roll(len(menus))]
# 決めた番号を返し、呼ばれたときの引数を記録する偽物です。
def fake_roll(n):
calls.append(n)
return 1
print(pick_menu(["カレー", "うどん", "ラーメン"], fake_roll))
print(calls)
# fake_roll が返す数を 2 に変えて、もう一度実行しましょう。
Python には偽物の道具もある
標準ライブラリの unittest.mock に、偽物を作る道具があります。Mock(return_value=9) は、呼ぶと 9 を返し、呼ばれた回数も覚えます。
次の1行は、偽物を1回呼んで、返した値と回数を出します。出力は 9 1 です。
標準の Mock を呼ぶ
python3 -c "from unittest import mock; m = mock.Mock(return_value=9); print(m(), m.call_count)"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
よくある間違い
注意
「何でも偽物にすれば、テストは速くて安全だ」と考える人が多くいます。偽物が本物と違う動きをすると、テストは通ってもバグが残ります。偽物にするのは、遅い、不安定、危険な部品だけにしましょう。