とある休日
ワイ「あ〜、今日は暇やな〜」
娘(5歳)「ねえパパ、相談したいことがあるんだけど」
ワイ「おお、娘ちゃん」
ワイ「ええで、何でも相談してや」
娘「実はね、幼稚園である遊びが流行ってるの」
ワイ「ほうほう、どんな遊びかな?」
娘「ええとね」
娘「サイコロを2回ふって、2回とも同じ目が出たら勝ちっていう遊びなの」
ワイ「おお、楽しそうやないかい」
娘「それでね、勝った人にはすご〜くいいことがあるの」
ワイ「ええ?すご〜くいいこと?」
ワイ「なんや気になるわ、教えてぇや」
娘「100万円もらえるの」
ワイ「ととと賭博やないかい!!!」
娘「しかも、そのあとね」
娘「なんやかんやで三食付きの施設にずっと無料で泊まれるの」
ワイ「いや、それブタ箱入っとるやないかい!!!」
娘「でもこの間、その遊びが幼稚園の規則で禁止されちゃったの・・・」
ワイ「いや法律で禁止されてんねん」
娘「それでね」
娘「今後はリアルマネーじゃなくてポイントを使って遊ぼうっていう話になったの」
ワイ「ん?ポイント?」
ワイ「どういうこと?」
娘「ええとね、Webアプリを作って」
娘「ブラウザ上でサイコロゲームができるようにして」
娘「お金じゃなくてポイントをやり取りして遊びたいの」
ワイ「おお、それはいい考えやな」
ワイ「それなら合法やからな」
そこでパパに相談
娘「それでね?」
娘「今そのWebアプリに必要な関数を書いてたところなの」
娘「どんな関数かって言うと・・・」
- サイコロを2回ふる
- 1回目と2回目の出目が同じなら
"同じだったよ!"
という文字列を返す- 出目が同じでなければ
"違ったよ!"
という文字列を返す
娘「↑こんな機能を持つ関数なんだけど、なんかうまく行かないの」
ワイ「ほうほう、書いた関数を見せてくれるか?」
娘「うん」
娘「まずは、1〜6のどれかをランダムに返してくれるrandomDice
っていう関数」
// 1〜6のどれかをランダムに返してくれる関数
const randomDice = () => {
return Math.floor(Math.random() * 6) + 1
}
ワイ「なるほど、この関数がサイコロとしての役割をするわけやな」
ワイ「ええ感じやん」
娘「もう1つ、目が同じだったかどうかをチェックする関数も作ったの」
// サイコロを2回ふって、目が同じだったかどうかをチェックする関数
const sameCheck = () => {
// サイコロをふる(1回目)
const roll1 = randomDice()
// サイコロをふる(2回目)
const roll2 = randomDice()
if (roll1 === roll2) {
return "同じだったよ!"
} else {
return "違ったよ!"
}
}
ワイ「なるほどな」
ワイ「さっきのrandomDice
関数を2回実行して」
ワイ「2回の結果が同じだったら"同じだったよ!"
」
ワイ「同じでなければ"違ったよ!"
を返す関数って訳やな」
娘「うん」
ワイ「特に問題ないんちゃう?」
娘「そうなんだけど・・・」
単体テストがしづらい
娘「この関数にね、今後も機能を追加する可能性があるの」
娘「だから、それに備えて自動テストの用意をしておきたいの」
ワイ「なるほど、機能を追加した際に」
ワイ「逆に今までの機能がおかしくなったりするとマズいから」
ワイ「ちゃんと全機能を網羅的にチェックする自動テストを準備しておこうってことやな」
娘「そう」
娘「でもさっきのsameCheck
関数は、何だかテストがしづらいの」
ワイ「そうなん?」
娘「うん」
娘「sameCheck
関数を実行するたびに」
娘「"同じだったよ!"
とか"違ったよ!"
っていう文字列が」
娘「ランダムに返ってきてることは確認できるんだけど」
- 1回目と2回目の出目が同じなら
"同じだったよ!"
という文字列を返す- 出目が同じでなければ
"違ったよ!"
という文字列を返す
娘「↑この条件を本当に満たしているのか、チェックできてない気がするの」
console.log(sameCheck())
// -> "違ったよ!"
console.log(sameCheck())
// -> "同じだったよ!"
console.log(sameCheck())
// -> "違ったよ!"
console.log(sameCheck())
// -> "違ったよ!"
console.log(sameCheck())
// -> "違ったよ!"
娘「↑ほらね」
ワイ「ああ、確かになぁ」
ワイ「どの目が出たことで同じになったのか、もしくはなってないのか」
ワイ「それが分からへんな」
娘「そうなの・・・」
よめ太郎「そんな時は、サイコロを引数としてぶち込んでやればええんや」
ワイ「おお、よめ太郎」
サイコロを引数として渡せばええんや
娘「サイコロを引数として渡す・・・?」
娘「あ、そっか・・・!」
娘「そういうことだよね!」
娘「じゃあ・・・」
娘「パパ、依存性を注入して?」
ワイ「ファッ!?」
ワイ「パパはちょっと(話題について行けなさ過ぎて)目眩がしてきたから」
ワイ「ここは、ママにやってみせてもらおうや・・・」
よめ太郎「え、ええで・・・」
実演開始
よめ太郎「さっきのsameCheck
関数を」
よめ太郎「ちょっといじって、引数を受け取れるようにしてやるんや」
// 引数を1つ受け取れるようにする。
const sameCheck = (dice) => {
// 引数で受け取ったサイコロをふる(1回目)
const roll1 = dice()
// 引数で受け取ったサイコロをふる(2回目)
const roll2 = dice()
if (roll1 === roll2) {
return "同じだったよ!"
} else {
return "違ったよ!"
}
}
よめ太郎「↑こんな感じや」
ワイ「ほお、さっきまでは引数を受け取らない関数やったけど」
ワイ「引数を1つ受け取るようになったんやね」
よめ太郎「せや」
よめ太郎「実行する時はこんな感じや」
const result = sameCheck(randomDice)
ワイ「なるほどな?」
ワイ「サイコロの代わりであるrandomDice
関数を、引数として渡してやって」
ワイ「それをsameCheck
関数が受け取って、内部で実行してやるわけか」
よめ太郎「そういうことや」
ワイ「でも、何でそんな回りくどいことすんの?」
テストの時にはサイコロを差し替えたい
よめ太郎「こうすることで、テストの時にサイコロを差し替えることができるんや」
よめ太郎「疑似サイコロにな」
ワイ「疑似サイコロ・・・」
ワイ「どういうこと?」
よめ太郎「ほな、コード書きながら説明するで」
よめ太郎「まずはな」
// 毎回1を返す関数
const mockDice1 = () => {
return 1
}
よめ太郎「↑こんな関数を作るんや」
よめ太郎「これは、毎回1
が出るテスト用の疑似サイコロみたいなもんや」
ワイ「ほうほう」
よめ太郎「そんで、この疑似サイコロを使って・・・」
// 疑似サイコロを使って、sameCheck関数をテストする。
// 必ず1が出る疑似サイコロを渡す。
const message = sameCheck(mockDice1)
// テストしてみる
if (message === "同じだったよ!") {
console.log("同じだったテスト成功!")
} else {
console.log("テスト失敗やわ・・・!!!")
}
よめ太郎「↑こうや」
よめ太郎「このコードを実行してみるで?」
同じだったテスト成功!
ワイ「おお、テスト成功したみたいやな1」
よめ太郎「せや」
よめ太郎「つまり・・・」
- 1回目と2回目の出目が同じなら
"同じだったよ!"
という文字列を返す
よめ太郎「↑この要件を満たしていることが確認できたやろ?」
よめ太郎「毎回必ず同じ目が出るサイコロやからな!」
ワイ「おお、なるほどな」
ワイ「出目をコントロールしながらsameCheck
関数のテストができるわけやな」
よめ太郎「せや」
「このサイコロを使って処理をしてくれ〜い!」
よめ太郎「って感じで、サイコロを渡して実行してもらう感じやな」
ワイ「なるほどな」
よめ太郎「ほな次は、目が同じじゃなかった場合のテストもしていくで」
よめ太郎「せやから・・・」
// 1と2を交互に返す関数
const alternateDice = (() => {
let num = 2
return () => {
num = num === 1 ? 2 : 1
return num
}
})()
よめ太郎「↑こうやな」
ワイ「なるほど、これは」
ワイ「1
と2
が交互に出る、不思議な疑似サイコロってことか」
ワイ「そんなこともできるんやな」
よめ太郎「せや」
よめ太郎「この疑似サイコロを使って・・・」
// 疑似サイコロを使って、sameCheck関数をテストする。
// 1と2が交互に出る疑似サイコロを渡す。
const message = sameCheck(alternateDice)
// テストしてみる
if (message === "違ったよ!") {
console.log("違った場合のテスト成功!")
} else {
console.log("テスト失敗やわ・・・!!!")
}
よめ太郎「↑こうやな」
よめ太郎「これも実行してみるで?」
違った場合のテスト成功!
ワイ「おお、テスト成功したな」
よめ太郎「せや」
よめ太郎「つまり・・・」
- 出目が同じでなければ
"違ったよ!"
という文字列を返す
よめ太郎「↑この仕様を満たしていることが確認できたわけや」
よめ太郎「1回目と2回目で必ず違う目が出るサイコロやからな」
ワイ「おお、なるほどなぁ・・・」
ワイ「サイコロを引数としてぶち込めるようにしておくことで」
ワイ「単体テストの時には疑似サイコロに差し替えることができるってことか・・・」
よめ太郎「せやな」
よめ太郎「sameCheck
関数の実行結果は、サイコロの結果に依存して変わるから」
よめ太郎「その依存先であるサイコロをコントロールできないと、テストがしづらいわけや」
ワイ「確かに・・・」
よめ太郎「せやから、依存先であるサイコロ関数を」
よめ太郎「sameCheck
関数の中でそのまま実行したら駄目なんや」
ワイ「なるほどな〜」
ワイ「依存先となるサイコロ君を、引数としてぶち込めるようにしておくことで」
ワイ「色んなパターンを試しながら自動テストができるわけか・・・」
よめ太郎「そういうことや」
よめ太郎「依存性の注入・・・**DI(Dependency Injection)**ってやつやな」
まとめ
- 関数から返ってくる値が一定でないと、単体テストがしづらい。
(乱数生成やAPI通信をする関数など) - だから、テストの時だけ一部をモック化したいことがある。
(今回で言えばサイコロ) - モック化したいものは、関数の中にそのまま書くのではなく、引数として注入できるようにしておく。
- そうすることで、単体テスト時にモックに差し替えることができる。
- すると、色んなパターンを網羅した自動テストが簡単に実現できる。
ワイ「↑ってことやな!」
よめ太郎「せやな!」
ちなみに
ワイ「依存性の注入っていう呼び方より、依存対象の注入とかの方がしっくりくるけど」
ワイ「Dependency Injectionの日本語訳は依存性の注入でええのんかいな」
よめ太郎「それなぁ・・・」
よめ太郎「依存性を注入してるっていう感じはしないけど、その訳し方で定着してもうたみたいやな」
ワイ「なるほどな」
ワイ「あと、もう一つ気になったんやけど」
ワイ「サイコロ以外にも何でも注入できそうやけど、それでエラー起こったりせえへんの?」
よめ太郎「ええところに気づいたな」
よめ太郎「そこは是非ともTypeScriptを導入して」
- サイコロとは、以下の条件を満たすものである
- 引数なしの関数であること
- 数値を返す関数であること
よめ太郎「↑こういった、型による縛りを加えた方がええやろな」
ワイ「なるほどな」
ワイ「サイコロのインターフェースを定義するんやな」
よめ太郎「そういうこっちゃ」
ワイ「よめ太郎、今日もおおきにやで」
ワイ「そろそろ15時やし」
ワイ「ワイも日本酒という依存対象を注入さしてもらうわ」
よめ太郎「そこの依存は断ち切らんかい」
〜おしまい〜
次の記事もよろしくやで
ゆめみアドベントカレンダー2日目は、goさんの記事やで!
AWS認定データアナリティクス専門知識(DAS)を、ビッグデータ専門知識の振り返りとして取得した試験準備やったことまとめ