test double patterns>test stub
DESCRIPTION
xutp読書会TRANSCRIPT
Test StubxUnit Test Patterns>Test Doube Patterns>
goyoki
Test Stub
• テストを実行する際、SUTの間接入力を操作するために、実際のオブジェクトと置換されるもの
How It Works
1. SUTの依存インターフェースに対して、テストに特化した実装を行う– 例えばSUT内のUntested Codeを実行するような実装を行う
2. SUTの実行前に、本物のDOCと同じ手順でTest StubをSUTに組み込む
3. テスト実行時は、Test Stubは指定値を間接入力としてSUTに渡す
4. 結果は直接出力を通常検証する
When to Use It
• 間接入力の存在によりUntested Codeが発生しているとき
• 使用不可なテスト環境を置換したいとき
留意
• Test DoubleをSUTに組み込む手段がないと話にならない
• 間接出力の検証が必要なときはMock ObjectやTest Spyを使う
Variation
• 紹介するVariation–Responder–Saboter–Temporary Test Stub–Procedural Test Stub
•他と異なる
–Entity Chain Snipping•コード改善テクニック
Variation:Responder
• 正常系の値を渡すTest Stub
• 主に“Happy Path” (正常系の実行パス)をテストしたいときに使用
• テストはSimple Success Testsになる
Variation:Saboter
• 異常系の値を渡すTest Stub
• 異常状態下でSUTがどのように問題に対処するかを検査する
• 実現方法
–想定外の値を返す/例外を投げる/ランタイムエラーを起こす
• 形態は複数– Simple Success Test/Expected Exception Test
Variation:Temporary Test Stub
• まだ利用不能なDOCの代替となるTest Stub
• 一般的な形:
–空の製品コードのクラスに特定値を返す実装をHard-Codeする
–DOCが確保されるとすぐに置き換えられる
Temporary Test StubとTDD
• TDDでOutside-In開発を行うときに使用
–空のクラスにコードを継ぎ足していき、最終的にTest Stubを製品コードに置き換えてしまう
–need-driven development(要求駆動開発/
ニーズ駆動開発)では、製品コードで置換しても、テストの保持のため、Mock ObjectとしてTest Doubleを残すことがある
Variation:Procedural Test Stub
• 手続き型プログラミング言語で使われるTest Stub
• 関数ポインタをサポートしていない開発言語では、主要なTest Doubleの実現手段となる
• しばしばTest Logic In Productionの状態となる
Variation:Entity Chain Snipping
•複雑な関係を持つオブジェクト群を置き換えてしまうもの
• Responderの一種
•一般的な利用目的:
–Fixuture Setupを早くしたいとき
–テストを理解しやすくしたいとき
Test Stubの注意点
• 製品とは異なる構成でSUTをテストしている–最低1回はTest Stubなしのテストを実行すべき
• 多用するとOver specified Softwareに陥る– Fragile Test等の問題につながる
• よくあるミス:–テストすべきSUTの一部をTest Stubに置き換えてしまう
–特にSUTの役割とテストフィクスチャの役割は明確に区別するよう注意しましょう
Implementation Notes
• ここでは2種類
–Hard-Coded Test Stub
–Configurable Test Stub
Hard-Coded Test Stub
•戻り値・例外等がプログラムロジックとしてHard-Codeされる
• 1つ、あるいはごく少数のテストに限定して使うときに使用
Configurable Test Stub
•複数のテストで別々にHard-Codeしたくないときに使う
• Fixture Setupでの作業の1つとして挙動を設定可能
• xUnitファミリーはツールを多く提供している
実装例
元のコード(だめな例)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 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 void testDisplayCurrentTime_whenever() {
// fixture setup
TimeDisplay sut = new TimeDisplay();
// exercise sut
String result = sut.getCurrentTimeAsHtmlFragment();
// verify outcome
Calendar time = new DefaultTimeProvider().getTime();
StringBuffer expectedTime = new StringBuffer();
expectedTime.append("<span class=¥"tinyBoldText¥">");
if ((time.get(Calendar.HOUR_OF_DAY) == 0)
&& (time.get(Calendar.MINUTE) <= 1)) {
expectedTime.append( "Midnight");
} else if ((time.get(Calendar.HOUR_OF_DAY) == 12)
&& (time.get(Calendar.MINUTE) == 0)) { // noon
expectedTime.append("Noon");
} else {
SimpleDateFormat fr = new SimpleDateFormat("h:mm a");
expectedTime.append(fr.format(time.getTime()));
}
expectedTime.append("</span>");
assertEquals( expectedTime, result);}
修正例(更にだめな例)public void testDisplayCurrentTime_whenever() {
// fixture setup
TimeDisplay sut = new TimeDisplay();
// exercise sut
String result = sut.getCurrentTimeAsHtmlFragment();
// verify outcome
Calendar time = new DefaultTimeProvider().getTime();
StringBuffer expectedTime = new StringBuffer();
expectedTime.append("<span class=¥"tinyBoldText¥">");
if ((time.get(Calendar.HOUR_OF_DAY) == 0)
&& (time.get(Calendar.MINUTE) <= 1)) {
expectedTime.append( "Midnight");
} else if ((time.get(Calendar.HOUR_OF_DAY) == 12)
&& (time.get(Calendar.MINUTE) == 0)) { // noon
expectedTime.append("Noon");
} else {
SimpleDateFormat fr = new SimpleDateFormat("h:mm a");
expectedTime.append(fr.format(time.getTime()));
}
expectedTime.append("</span>");
assertEquals( expectedTime, result);}
・時間によって実行パスが変わる
・テストを書いているのではなく、SUTのコピーを実装している
修正
• 時間の間接入力を望ましいものに操作可能にする
–まずシステムクロック依存部分:TimeProviderをTest Doubleに置換できるようリファクタリングする
例1 Hand-Coded Test StubによるResponderpublic void testDisplayCurrentTime_AtMidnight() throws Exception {
// Fixture setup:// Test Double configurationTimeProviderTestStub tpStub = new TimeProviderTestStub();tpStub.setHours(0);tpStub.setMinutes(0);// Instantiate SUT:TimeDisplay sut = new TimeDisplay();// Test Double installationsut.setTimeProvider(tpStub);// exercise sutString result = sut.getCurrentTimeAsHtmlFragment();// verify outcomeString expectedTimeString = "<span class=¥"tinyBoldText¥">Midnight</span>";assertEquals("Midnight", expectedTimeString, result);
}
例2 Dynamically GeneratedによるResponderpublic void testDisplayCurrentTime_AtMidnight_JM() throws Exception {
// Fixture setup:TimeDisplay sut = new TimeDisplay();// Test Double configurationMock tpStub = mock(TimeProvider.class);Calendar midnight = makeTime(0,0);
tpStub.stubs().method("getTime").withNoArguments().will(returnValue(midnight));// Test Double installationsut.setTimeProvider((TimeProvider) tpStub);// exercise sutString result = sut.getCurrentTimeAsHtmlFragment();// verify outcomeString expectedTimeString = "<span class=¥"tinyBoldText¥">Midnight</span>";assertEquals("Midnight", expectedTimeString, result);
}
例3 Anonymous Inner ClassによるSaboteurpublic void testDisplayCurrentTime_exception() throws Exception {
// fixture setup// Define and instantiate Test StubTimeProvider testStub = new TimeProvider()
{ // anonymous inner Test Stubpublic Calendar getTime() throws TimeProviderEx {
throw new TimeProviderEx("Sample");}
};// Instantiate SUT:TimeDisplay sut = new TimeDisplay();sut.setTimeProvider(testStub);// exercise sutString result = sut.getCurrentTimeAsHtmlFragment();// verify direct outputString expectedTimeString = "<span class=¥"error¥">Invalid Time</span>";assertEquals("Exception", expectedTimeString, result);
}
番外: Entity Chain Snipping
• こんがらがったオブジェクトの連なりを置換する
適用前public void testInvoice_addLineItem_noECS() {
final int QUANTITY = 1;Product product = new Product(getUniqueNumberAsString(), getUniqueNumber());State state = new State("West Dakota", "WD");City city = new City("Centreville", state);Address address = new Address("123 Blake St.", city, "12345");Customer customer= new Customer(getUniqueNumberAsString(), getUniqueNumberAsString(),
address);Invoice inv = new Invoice(customer);// Exerciseinv.addItemQuantity(product, QUANTITY);// VerifyList lineItems = inv.getLineItems();assertEquals("number of items", lineItems.size(), 1);LineItem actual = (LineItem)lineItems.get(0);LineItem expItem = new LineItem(inv, product, QUANTITY);assertLineItemsEqual("",expItem, actual);
}
適用後public void testInvoice_addLineItem_ECS() {
final int QUANTITY = 1;Product product = new Product(getUniqueNumberAsString(), getUniqueNumber());Mock customerStub = mock(ICustomer.class);customerStub.stubs().method("getZone").will(returnValue(ZONE_3));Invoice inv = new Invoice((ICustomer)customerStub.proxy());// Exerciseinv.addItemQuantity(product, QUANTITY);// VerifyList lineItems = inv.getLineItems();assertEquals("number of items", lineItems.size(), 1);LineItem actual = (LineItem)lineItems.get(0);LineItem expItem = new LineItem(inv, product, QUANTITY);assertLineItemsEqual("", expItem, actual);
}