xUnit Test Patterns - Chapter19
-
Upload
takuto-wada -
Category
Technology
-
view
8.691 -
download
2
description
Transcript of xUnit Test Patterns - Chapter19
Chapter 19.xUnit Basic
Patterns
● Test Definition● Test Method
– Four-Phase Test● Assertion Method
– Assertion Message● Testcase Class
● Test Execution● Test Runner● Testcase Object● Test Suite Object● Test Discovery● Test Enumeration● Test Selection
Test Method
How It Works(1)
● テストコードってどこに書くの?● ひとつひとつのテスト毎にメソッド (Test Method) にし
てクラスに配置しましょう
● How It Works● 各テストをメソッド/手続き/関数のかたちで Four-
Phase(358) の実装を行い、Fully Automated Test とする。
● 大事なのは、 assertion を書いて自己テストコード (Self-Checking Test:26) とすること
How It Works(2)
● Test Method には標準 Template がある● Simple Success Test
– 正常系のテスト。 Fixture setup から result verification まで一本道
● Expected Exception Test– 例外系のテスト
● Constructor Test– オブジェクトを作成し属性をテストするだけのテスト
Why We Do This
● 手続き型言語の場合● Test Method をファイルやモジュールに書く
● オブジェクト指向言語の場合● Test Method を Testcase Class(373) の中に書
き、Test Discovery(393) や Test Enumeration(399) を使って Test Method を Testcase Object(382) としてインスタンス化する
● 標準 template に従うことでテストを読みやすくシンプルにし、 SUT の動くドキュメントとすることができる
Implementation Notes● どう仕組みを実装する?
● Static method として実装し呼び出しを列挙する– テスト結果を集めたりする共通化がやりにくい
● Test Method 一つ一つに対応する Test Suite Object(387)をつくる– Test Discovery や Test Enumaration でインスタンス化する
場合に便利● 静的型付け言語の場合はメソッドに “throws
Exception” などを書かなければならない– コンパイラに対して「この例外は Test Runner が処理するよ」
という意思表示になる● 殆どの Test Method は3パターンに分類できる
Simple Success Test● ソフトウェアには正常系 “happy path” があ
る。Simple Success Test はそれを書く● SUT をインスタンス化して叩き、結果を assert
– 言い換えると、 Four-Phase に則ったテストを書く● 例外はキャッチせず、 Test Automation Framework ま
で貫通させる– テストの中で例外を扱うと Obscure Test や誤解のもと– Tests as Documentation の原則を思い出そう– Try-catch を書かない利点は他にもあって、 Test
Automation Framework が例外発生行を特定しやすくなること
public void testFlightMileage_asKm() throws Exception { //set up fixture Flight newFlight = new Flight(validFlightNumber); try { //exercise SUT newFlight.setMileage(1122); //verify results int actualKilometres = newFlight.getMileageAsKm(); int expectedKilometres = 1810; //verify results assertEquals( expectedKilometres, actualKilometres); } catch (InvalidArgumentException e) { fail(e.getMessage()); } catch (ArrayStoreException e) { fail(e.getMessage()); }}
Simple Success Test のダメな例
不要な try/catch
public void testFlightMileage_asKm() throws Exception { //set up fixture Flight newFlight = new Flight(validFlightNumber); newFlight.setMileage(1122); //exercise mileage translator int actualKilometres = newFlight.getMileageAsKm(); //verify results int expectedKilometres = 1810; assertEquals( expectedKilometres, actualKilometres);}
Simple Success Test の良い例
xUnit は unexpected exception を失敗として扱えるので、
throws Exception しておけばよい
Expected Exception Test (1)● 多くの不具合は正常系以外のパスに潜む。特に例
外系のシナリオ。それは、● Untested Requirements (268) や、● Untested Code (268) であったりするため
● Expected Exception Test はわざと SUT が例外を出すようなテストを書き、きちんと例外が出ることを調べる● 例外の中身も調べたいときは Equality Assertion で調
べる● 例外が出なかったときは fail メソッドなどでテストを失
敗させる
Expected Exception Test (2)● 想定される (expected) 例外には、テストを書いた
ほうがよい● 再現が難しいが、出るかもしれない (might raise)
例外には、テストを書かなくてよい● (★ ネットワーク障害とか、 Disk full とか)● そういう例外は Simple Success Test の失敗として現
れるため● もしそういう例外もテストしたいなら、 Test Stub から例
外を発生させてテストする
Expected Exception Test (3)● 例外をテストするときの仕組み
● JUnit 3.x– ExpectedException クラスを継承させる (?)
● 小さいテストクラスが沢山できるし、あまり旨味は無い
● JUnit 4.x, NUnit– Test Method の annotation/attribute に書く
● Block のある言語 (Smalltalk, Ruby, …)– Block で例外が発生するか調べるテストを書ける
public void testSetMileage_invalidInput() throws Exception { //set up fixture Flight newFlight = new Flight(validFlightNumber); //exercise SUT newFlight.setMileage(-1122); //invalid //how do we verify an exception was thrown?}
Expected Exception Test のダメな例
想定された例外なのにテストが失敗してしまう
public void testSetMileage_invalidInput()throws Exception { //set up fixture Flight newFlight = new Flight(validFlightNumber); try { //exercise SUT newFlight.setMileage(-1122); fail("Should have thrown InvalidInputException"); } catch(InvalidArgumentException e) { //verify results assertEquals( "Flight mileage must be positive", e.getMessage()); }}
Expected Exception Test の良い例
想定される例外を catch する例外が出なかった場合は fail させる
public void testSetMileage_invalidInput2() throws Exception { //set up fixture Flight newFlight = new Flight(validFlightNumber); try { //exercise SUT newFlight.setMileage(-1122); //cannot fail() here if SUT throws same kind of exception } catch(AssertionFailedError e) { //verify results assertEquals( "Flight mileage must be positive", e.getMessage()); return; } fail("Should have thrown InvalidInputException");}
Expected Exception Test の特殊例?
Fail メソッドが投げる例外と同じ例外を SUT が投げる場合にはこう書くしかない !?
[Test] [ExpectedException(typeof( InvalidArgumentException), "Flight mileage must be > zero")]public void testSetMileage_invalidInput_AttributeWithMessage() { //set up fixture Flight newFlight = new Flight(validFlightNumber); //exercise SUT newFlight.setMileage(-1122);}
Method Attribute を使った EET
Smalltalk:
testSetMileageWithInvalidInput self should: [Flight new mileage: -1122] raise: RuntimeError new 'Should have raised error'
Ruby:
def testSetMileage_invalidInput flight = Flight.new() assert_raises( RuntimeError, "Should have raised error") do flight.setMileage(-1122) endend
Block を使った EET
describe Flight do before do @flight = Flight.new end
it "Should have raised error" do lambda { flight.setMileage(-1122) }.should_raise(RuntimeError) end
end
Rspec でやってみる
Constructor Test● Fixture Setup phase で作成されたオブジェクトが
正しく作成されているかを各 Test Method で調べていると Test Code Duplication (213) がひどくなる● オブジェクト作成のテストだけ別の Test Method にす
ることで他のテストをシンプルにすることができる● Defect Localication にもなる
– 各属性の Test Method を分けるとさらに Defect Localization
● Constructor Test は Simple Success Test の形をとる場合もあるし、 Expected Exception Test の形をとる場合もある
Dependency Initialization Test● Constructor Test の亜種● 置き換え可能な依存があるオブジェクトがある場
合、本番環境では本物の依存オブジェクトが参照されることをテストする
● ふつうの Constructor Test と分けて管理した方がよい
public void testFlightMileage_asKm2() throws Exception { //set up fixture //exercise constructor Flight newFlight = new Flight(validFlightNumber); //verify constructed object assertEquals(validFlightNumber, newFlight.number); assertEquals("", newFlight.airlineCode); assertNull(newFlight.airline); //set up mileage newFlight.setMileage(1122); //exercise mileage translator int actualKilometres = newFlight.getMileageAsKm(); //verify results int expectedKilometres = 1810; assertEquals( expectedKilometres, actualKilometres); //now try it with a canceled flight newFlight.cancel(); try { newFlight.getMileageAsKm(); fail("Expected exception"); } catch (InvalidRequestException e) { assertEquals( "Cannot get cancelled flight mileage", e.getMessage()); } }
Constructor Test のダメな例
いろいろやりすぎでピントがあっていないEager Test
public void testFlightConstructor_OK() throws Exception { //set up fixture //exercise SUT Flight newFlight = new Flight(validFlightNumber); //verify results assertEquals( validFlightNumber, newFlight.number ); assertEquals( "", newFlight.airlineCode ); assertNull( newFlight.airline );}
Constructor Test の良い例1 正常系
public void testFlightConstructor_badInput() { //set up fixture BigDecimal invalidFlightNumber = new BigDecimal(-1023); //exercise SUT try { Flight newFlight = new Flight(invalidFlightNumber); fail("Didn't catch negative flight number!"); } catch (InvalidArgumentException e) { //verify results assertEquals( "Flight numbers must be positive", e.getMessage()); }}
Constructor Test の良い例2 異常系
public void testFlightMileage_asKm() throws Exception { //set up fixture Flight newFlight = new Flight(validFlightNumber); newFlight.setMileage(1122); //exercise mileage translator int actualKilometres = newFlight.getMileageAsKm(); //verify results int expectedKilometres = 1810; assertEquals( expectedKilometres, actualKilometres);}
Constructor Test があると他のテストのピントがはっきりする
インスタンス化直後の assertion は不要
Four-Phase Test
● Test Definition● Test Method
– Four-Phase Test● Assertion Method
– Assertion Message● Testcase Class
● Test Execution● Test Runner● Testcase Object● Test Suite Object● Test Discovery● Test Enumeration● Test Selection
Four-Phase Test● 良いテストには4つの Phase がある
● Setup● Exercise● Verify● Teardown
● ワンパターン = テストの読みやすさ = Tests as Documentation● Test Method の中身はテスト内容に集中すべし
Four-Phase Test● どう setup/teardown する?
● Testcase Class per Class または Testcase Class per Feature の場合– In-line Setup– Garbase-Collected Teardown または In-line Teardown
● Testcase per Fixture の場合– Implicit Setup
● 例えば setUp メソッド
– Implicit Teardown● 例えば tearDown メソッド
public void testGetFlightsByOriginAirport_NoFlights_inline() throws Exception { //Fixture setup NonTxFlightMngtFacade facade =new NonTxFlightMngtFacade(); BigDecimal airportId = facade.createTestAirport("1OF"); try { //Exercise system List flightsAtDestination1 = facade.getFlightsByOriginAirport(airportId); //Verify outcome assertEquals( 0, flightsAtDestination1.size() ); } finally { //Fixture teardown facade.removeAirport(airportId ); }}
例: Four-Phase Test (In-line)
NonTxFlightMngtFacade facade = new NonTxFlightMngtFacade();private BigDecimal airportId;
protected void setUp() throws Exception { //Fixture setup super.setUp(); airportId = facade.createTestAirport("1OF");}
public void testGetFlightsByOriginAirport_NoFlights_implicit() throws Exception { //Exercise SUT List flightsAtDestination1 = facade.getFlightsByOriginAirport(airportId); //Verify outcome assertEquals( 0, flightsAtDestination1.size() );}
protected void tearDown() throws Exception { //Fixture teardown facade.removeAirport(airportId); super.tearDown(); }
例: 4PT (Implicit Setup/Teardown)
Assertion Method
● Test Definition● Test Method
– Four-Phase Test● Assertion Method
– Assertion Message● Testcase Class
● Test Execution● Test Runner● Testcase Object● Test Suite Object● Test Discovery● Test Enumeration● Test Selection
Assertion Method● どうやってテストに自己チェックさせるか
● ユーティリティメソッドを呼ぶことで望んだ結果になったかどうかの評価をすればいい
● Fully Automated Tests(26)の肝は、テストをSelf-Checking Tests(26)にすること● そのためには期待する結果を表現し、自動でチェックす
ることが必要
● Assertion Method は期待する結果を表現し、● コンピュータにとっては実行可能に● 人間には Tests as Documentation(23) になる。
Why We Do This● 期待する結果を Conditional Test Logic(200)で
表現すると…● 饒舌に過ぎ、読むのも理解するのも難しい● Test Code Duplication(213)が発生しやすい● Buggy Test(260)も発生しやすい
● Assetion Method はこの問題を…● 再利用性の高い Test Utility Methods(599)に複雑さ
を移すことにより解決する● そのメソッドの正しさは Test Utility Tests(599)でテス
トすることも可能
if (x.equals(y)) { throw new AssertionFailedError( "expected: <" + x.toString() + "> but found: <" + y.toString() + ">");} else { // Okay, continue // ...}
// 上の例では NPE が発生するのでガード節を入れてみると…
if (x == null) { //cannot do null.equals(null) if (y == null ) { //they are both null so equal return; } else { throw new AssertionFailedError( "expected null but found: <" + y.toString() +">"); } } else if (!x.equals(y)) { //comparable but not equal! throw new AssertionFailedError( "expected: <" + x.toString() + "> but found: <" + y.toString() + ">"); } //equal
まずはひどい例から
/** * Asserts that two objects are equal. If they are not, * an AssertionFailedError is thrown with the given message. */ static public void assertEquals(String message, Object expected, Object actual) { if (expected == null &&actual == null) return; if (expected != null && expected.equals(actual)) return; failNotEquals(message, expected, actual); }
---------------------------------
assertEquals( x, y ); // 呼び出し側はこれだけ!!
JUnit はこうリファクタリングした
Implementation Notes● 全ての xUnit ファミリーは Assetion Method を備
えているが、考えるべきことはある● Assertion Method をどうやって呼ぶか● 最適な Assertion Method をどう選ぶか● Assertion Message(370) に何を書くか
Calling Built-in Assertion Methods● Test Method(348) からテストフレームワーク組み込みで提供されている Assertion Method を呼ぶには…● フレームワークが提供する Testcase Superclass(638)
を継承する (JUnit タイプ)● グローバルクラス/モジュールを完全修飾名で呼び出す
(NUnit タイプ)● Mixin (Test::Unit タイプ)● マクロ (CppUnit タイプ)
Assertion Messages● テスト失敗時の出力に含めるメッセージ
● どのテストが失敗したかをわかりやすくする– Assertion Roulette 参照
● 省略可能な引数として Assertion Method に渡すかたちが多い
● テスト失敗時に「なぜ失敗したか」の情報が多ければデバッグは容易になる● 正しい Assertion Method を選ぶことはエラー時のメッセージ出力を適切にする意味でも重要
● 問題なのは、 Assertion Message の引数の順番が xUnit 毎にブレていること
Choosing the Right Assertion● Assertion Method には二つのゴールがある
● 期待しない結果のときにはテストを失敗させること● SUT がどう振る舞うかのドキュメントになること
● これらのゴールを満たすため、最適な Assertion Method を選ぶことが重要になる
● Assertion Method には以下のカテゴリがある● Single-Outcome Assertions● Stated Outcome Assertions● Expected Exception Assertions● Equality Assertions● Fuzzy Equality Assertions
Equality Assertion● 結果が期待値と等価かどうかを調べる
● もっとも使われる Assertion Method
● 引数の順番は規約としては expected, actual の順番● 順番は失敗時のメッセージに関係するので重要● ★この順番でない xUnit もある。(NUnit とか)● ★自分の使う Equality Assertion の順を覚えよう
● 内部では等価性を調べるメソッドが呼ばれる● Java では equals とか● SUT ごと調べたい場合は Test-Specific Subclass
assertEquals( expected, actual ); // since JUnit3.x
assertThat( actual, is(expected) ); // since JUnit 4.4
Assert.AreEqual( actual, expected ); // NUnit
is( actual, expected ); // Test::Simple (Perl)
equals( actual, expected ); // QUnit
actual.should == expected // Rspec
Equality Assertion いろいろ
Fuzzy Equality Assertion
● 結果と期待値との完全な一致が難しい場合● 浮動小数点をあつかうとき● 期待値と完全一致させるには結果に本質的でない不要
なゴミが多いとき (XML の空白ノードとか)
assertEquals( 3.1415, diameter/2/radius, 0.001);
assertEquals( expectedXml, actualXml, elementsToCompare );
Stated Outcome Assertion
● 期待値を渡す必要がないとき● Conditional Test Logic を避けるためのガード節
としても使える
assertNotNull(a );
assertTrue(b > c );
assertNonZero(b );
Expected Exception Assertion
● ブロックやクロージャを備えている言語は発生するであろう例外をパラメータとして渡す Assertion Method が使える
self should: [Flight new mileage: -1122] raise: RuntimeError new 'Should have raised error'
assert_raises( RuntimeError, "Should have raised error") { flight.setMileage(-1122) }
assert_raises( RuntimeError, "Should have raised error")do flight.setMileage(-1122) end
Single-Outcome Assertion
● 常に同じ振る舞いをする Assertion Method● 例えば fail メソッド
● 使われる状況● まだ完成していないテスト Unfinished Test Assertion を示す● Expected Exception Test の中の try/catch ブロックで使う
fail( "Expected an exception" );
unfinishedTest();
public void testSetMileage_invalidInput()throws Exception { //set up fixture Flight newFlight = new Flight(validFlightNumber); try { //exercise SUT newFlight.setMileage(-1122); fail("Should have thrown InvalidInputException"); } catch(InvalidArgumentException e) { //verify results assertEquals( "Flight mileage must be positive", e.getMessage()); }}
Single-Outcome Assertion の例
想定される例外を catch する例外が出なかった場合は fail させる
Assertion Message
● Test Definition● Test Method
– Four-Phase Test● Assertion Method
– Assertion Message● Testcase Class
● Test Execution● Test Runner● Testcase Object● Test Suite Object● Test Discovery● Test Enumeration● Test Selection
Assertion Message● どの Assertion Method が落ちたか知りたい
● Assertion Method 毎にメッセージ引数を渡す
● テスト失敗時の出力に含めるメッセージ● どのテストが失敗したかをわかりやすくする
– Assertion Roulette 参照● 省略可能な引数として Assertion Method に渡すかた
ちが多い
When to Use It● 二つの学派(School)がある
● テスト駆動派 (Test drivers) と、その他派
● テスト駆動派● “single assertion per Test Method”● Test Method に Assertion Method がひとつしかな
いので、どの Assertion Method が落ちたかは自明。故に Assertion Message は不要。
● その他派● ひとつの Test Method に複数 Assertion Method が
あるので、 Assertion Message を使いたくなる
Implementation Notes● Assertion Message に何を書くべきか
● Assertion-Identifying Message● Expectation-Describing Message● Argument-Describing Message
Assertion-Identifying Message● 同種の Assertion Method が複数ある場合にどれ
が失敗したか分かりにくい● Assertion Method 毎に違う文字列を渡して識別する
● 識別に使う文字列の例● Assertion Method に使う変数名
– 名前に悩む必要がないので便利かも● 単なる連番
– テストコードを読まないと結局どこが失敗したかわからなかったりする
Expectation-Describing Message● テストが失敗した時に「実際何が起こったか」はわ
かる。だが「何が起こるべきだったか」はわからない。● テストコード内にコメントを書く手もある
● もっと良いのは Assertion Message に期待値の説明を書くこと● Equality Assertion の場合は自動でやってくれるので必要無し
● Stated Outcome Assertion の場合は入れなければわからない
Argument-Describing Message● いくつかの Assertion Method は失敗時の出力が
不親切● 特に Stated Outcome Assertion
– assertTrue(式) とか– 失敗したのはわかるが、どんな式が失敗したのかわからない– 式を Assertion Message に含めてしまう手がある
assertTrue( "Expected a > b but a was '" + a.toString() + "' and b was '" + b.toString() + "'", a.gt(b) ); assertTrue( "Expected b > c but b was '" + b.toString() + "' and c was '" + c.toString + "'", b > c ); }
Argument-Describing Messageの例
ご清聴ありがとうございました