3. Dependency Injection
● SUT をどう設計すれば、ランタイムに依存オブジェ
クトを差し替えられるだろうか?
● クライアントが SUT に対して依存オブジェクトを提供す
れば良い
● テストする際には、 SUT を依存から独立してテストでき
るのが望ましいが、依存クラス名などがハードコードさ
れていると難しい
● Dependency Injection は、 SUT に対する依存オブ
ジェクトをテスト時に差し替えるのに使える
4. How It Works
● クライアントコードやシステムの設定部(ファイル/
コード)で、 SUT が実行時に依存するオブジェクトを
渡せるようにする
● この設計のポイントは、 依存が “front door” から
渡せるようになっているところ
● つまり、「依存を渡す」ということも SUT の API の一部
になる
● 依存の渡し方は、メソッドの引数、コンストラクタ、 setter
などがある。
5. When to Use It
● まずは、テスト時に DOC を Test Double に差し替
えるときに使う
● Static binding は依存クラス名を SUT に直接書いてし
まうことで、採れる手段を大幅に狭めてしまう
● Dynamic binding は依存クラスの解決を実行時まで遅
延することで、柔軟さを得ることができる
● Dependency Injection(DI) はコードをスクラッチ
から設計する際には良い手段となる
● 特に TDD で設計している場合には、 DOC を Test
Double で差し替えたくなるので、自然と DI が必要にな
る
6. Implementation Notes(1)
● 二つの解決すべき問題がある
● Test Double をどんな DOC の場合にも使えるように
する必要がある。これは静的型付け言語の場合には、
Test Double をいかにも DOC のように(型を矛盾させ
ること無く)渡せなければならないことを意味する
● SUT に Test Double を渡す手段を提供しなければな
らない
7. Implementation Notes(2)
● 型の互換性
● (特に静的型付け言語で) DI を実現するには、 DOC と
Test Double が Type Compatible でなければならな
い
● 静的型付け言語では、 DOC と Test Double が共通の
interface を実装していること
● 動的型付け言語では、 DOC と Test Double が同じ
signature (メソッド名など) を持っていること
● 既存のコードに対して DI パターンを導入するには、まず
DOC に対して「インターフェイスの抽出」を行い、その抽
出された interface に対して Test Double を書けば良
い
8. Implementation Notes(3)
● Test Double の渡し方
● さまざまなやり方があるが、名前/型のハードコードを止
め、実行時まで解決を遅らせるという基本は変わらない
– Parameter Injection
● 使いたいメソッドの引数として DOC も渡す
– Constructor Injection
● SUT 構築時に DOC を渡す
– Setter Injection
● どこかのタイミングで setter 経由で DOC を SUT に渡す
● IoC フレームワーク (DI コンテナ) を使うと、 DI の仕組
みの実装自体も作らずに済み、つまり SUT や DOC は
DI の実装に対して非依存になり、使いまわしが効くよう
になる
9. Motivating Example
// リファクタリング前のコード
public void testDisplayCurrentTime_AtMidnight() {
//fixture setup
TimeDisplay sut = new TimeDisplay();
//exercise SUT
String result = sut.getCurrentTimeAsHtmlFragment();
//verify direct output
String expectedTimeString =
"<span class="tinyBoldText">Midnight</span>";
assertEquals( expectedTimeString, result);
}
public String getCurrentTimeAsHtmlFragment() {
Calendar currentTime;
try {
currentTime = new DefaultTimeProvider().getTime();
} catch (Exception e) {
return e.getMessage();
}
// etc.
}
10. Parameter Injection
// テストコード
public void testDisplayCurrentTime_AtMidnight_PI() {
//Fixture setup
//Test Double instantiation
TimeProvider tpStub = new MidnightTimeProvider();
//Instantiate SUT
TimeDisplay sut = new TimeDisplay();
//Exercise SUT using Test Double
String result = sut.getCurrentTimeAsHtmlFragment(tpStub);
//Verify outcome
String expectedTimeString =
"<span class="tinyBoldText">Midnight</span>";
assertEquals("Midnight", expectedTimeString, result);
}
//プロダクトコード
public String getCurrentTimeAsHtmlFragment(TimeProvider timeProviderArg) {
Calendar currentTime;
try {
currentTime = timeProviderArg.getTime();
} catch (Exception e) {
return e.getMessage();
}
// etc.
}
11. Constructor Injection
// テストコード
public void testDisplayCurrentTime_AtMidnight_CI() throws Exception {
//Fixture setup
//Test Double instantiation
TimeProvider tpStub = new MidnightTimeProvider();
//Instantiate SUT injecting Test Double
TimeDisplay sut = new TimeDisplay(tpStub);
//Exercise SUT
String expectedTimeString =
"<span class="tinyBoldText">12:01 AM</span>";
//Verify outcome
assertEquals("12:01 AM",
expectedTimeString,
sut.getCurrentTimeAsHtmlFragment());
}
//プロダクトコード
public class TimeDisplay {
private TimeProvider timeProvider;
public TimeDisplay() { //backwards compatible constructor
timeProvider = new DefaultTimeProvider();
}
public TimeDisplay(TimeProvider timeProvider) { //new constructor
this.timeProvider = timeProvider;
}
...
}
12. Setter Injection(1)
// テストコード
public void testDisplayCurrentTime_AtMidnight_SI()
throws Exception {
//Fixture setup
//Test Double instantiation
TimeProvider tpStub = new MidnightTimeProvider();
//Instantiate SUT
TimeDisplay sut = new TimeDisplay();
//Test Double installation
sut.setTimeProvider(tpStub);
//Exercise SUT
String result = sut.getCurrentTimeAsHtmlFragment();
//Verify outcome
String expectedTimeString =
"<span class="tinyBoldText">Midnight</span>";
assertEquals("Midnight", expectedTimeString, result);
}
13. Setter Injection(2)
// プロダクトコード
public class TimeDisplay {
private TimeProvider timeProvider;
public TimeDisplay() {
timeProvider = new DefaultTimeProvider();
}
public void setTimeProvider(TimeProvider provider) {
this.timeProvider = provider;
}
public String
getCurrentTimeAsHtmlFragment() throws TimeProviderEx {
Calendar currentTime;
try {
currentTime = getTimeProvider().getTime();
} catch (Exception e) {
return e.getMessage();
}
// etc.
}
...
}