Skip to content

Instantly share code, notes, and snippets.

@euske
Last active July 31, 2024 08:16
Show Gist options
  • Save euske/a3659d7609d704ed9ef07b9f1431f093 to your computer and use it in GitHub Desktop.
Save euske/a3659d7609d704ed9ef07b9f1431f093 to your computer and use it in GitHub Desktop.
依存注入 (Dependency Injection) と test doubles

依存注入 (Dependency Injection) と test doubles

依存の注入とは何か?

たとえば、新しく作ったウェブブラウザを自動テストすることを考えてみよう。 このブラウザはキーボードとマウスからの入力に従って動作し、画面を出力するものとする。

          +-------------+
          |             |
keyboard --->           |
          |   browser  ---> display
   mouse --->           |
          |             |
          +-------------+

これをコードで表すとこんな感じになる。

display = browser(keyboard, mouse)

このブラウザを自動的にテストするには、あらかじめ用意したキーボードとマウスの列を与え、 画面出力をあらかじめ用意した画面と比較すればよいだろうか?

言い換えると、キーボードとマウスの入力が決まれば、ブラウザの画面表示は必ず決まるだろうか?

keyboard = ["ENTER"]
mouse = ["↑", "↑", "←", "←", "←"]
display = browser(keyboard, mouse)
assertEquals(display, "本日は晴れ")

実はそうではない。ブラウザはインターネットにつながっているので、画面表示はネットの 内容によって毎回変わるためだ (天気予報を考えてみよう)。

つまり実際にはこうなっている:

          +-------------+
          |             |
keyboard --->           |
          |   browser  ---> display
   mouse --->           |
          |             |
(internet)-->           |
          |             |
          +-------------+

ブラウザの挙動は、(上の関数には書かれていないが) 暗黙のうちにインターネットの内容に 依存している (dependency)。 このような関数をテストするひとつの方法として、テスト時だけは微妙に異なる挙動をする「テストモード」を追加する方法がある。

テスト時:

display = browser(keyboard, mouse, testmode=true)

本番時:

display = browser(keyboard, mouse, testmode=false)

しかしこれは理想的な方法ではない。モードの切り替えによってコードの挙動が微妙に変わるため、 テストで正しく動いたからといって、本番でも正しく動くかどうかは保証できないのである。 理想は テストでも本番でも、まったく同じコードを使うこと である。

これを解決するために、別の方法を考える。関数を変更して、キーボードとマウスだけではなく、 インターネット全体を明示的に外から入力として与える 以下のようなコードに変更できないだろうか?

display = browser(keyboard, mouse, internet)
                                   ^^^^^^^^ Dependency Injection

これが 依存の注入 (dependency injection, 通称 DI) である。 ここで注入されているのはインターネット全体である。 (これを実際にどうやって実現するかは後述する)

ここではブラウザを例に挙げたが、一般的に依存注入は もっと小さなコンポーネントをテストするときに使われる。 依存注入により、各コンポーネントを単体で (unit)、 個別にテストすることが可能になる。

Test doubles とは何か?

さて、上のコードはこのままでは明らかにテストできない。 そこで、テスト用の 偽物のインターネット を作成する。 これはいつも必ず同じ内容を返すページが数個だけ入っているものとする。 このような偽物のインターネットをあらかじめ作成しておけば、それを使って自動テストが可能となる。

テスト時:

keyboard = ["ENTER"]
mouse = ["↑", "↑", "←", "←", "←"]
fakeinternet = new FakeInternet()
display = browser(keyboard, mouse, fakeinternet)
assertEquals(display, "本日は晴れ")

本番時:

keyboard = 本物のキーボード入力
mouse = 本物のマウス入力
internet = 本物のインターネット接続
display = browser(keyboard, mouse, internet)

ここで使われる偽物のインターネット (FakeInternet) のようなものを test doubles (テスト用の双子) と呼ぶ。 これは本物とそっくりの動きをするが、(テスト目的のため) つねに決まった内容を返すようなコンポーネントである。 関数 browser は入力が本物かどうかは検知せず、本物の入力に対しても偽物の入力に対しても同様に動作する。 Test doubles を使うことによって、 テストでも本番でも、まったく同じコードを使うこと が可能になる。

Test doubles は「偽物の程度」によって、いくつかの種類に分けられている:

  • dummy: コンパイルを通すだけの実体がないコード。(実際に使われた場合はエラーを返す)
  • stub: つねに同じ値を返すだけの機能。
  • spy: どのように使われたかの情報を記録する機能が入っている。
  • mock: どのように使われたかをチェックする機能が追加されている。
  • fake: 実際の動きに近い偽物として設計されたもの。

cf. https://jesusvalerareales.medium.com/testing-with-test-doubles-7c3abb9eb3f2

Test doubles の欠点

このように役に立つ test doubles であるが、欠点も存在する:

  • 本物のコードに加えてテスト用の stub や spy を実装せねばならず、余分に手間がかかる。
  • コードがあちこちに分散するので、処理の流れが追いにくくなる。そのプログラミング言語に慣れていないときは、これはとくに顕著な障害となりうる。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment