xunit test patterns - chapter19

58
Chapter 19. xUnit Basic Patterns

Upload: takuto-wada

Post on 11-May-2015

8.690 views

Category:

Technology


2 download

DESCRIPTION

handout for xUnit Test Patterns Reading Group Japan

TRANSCRIPT

Page 1: xUnit Test Patterns - Chapter19

Chapter 19.xUnit Basic

Patterns

Page 2: xUnit Test Patterns - Chapter19

● 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

Page 3: xUnit Test Patterns - Chapter19

Test Method

Page 4: xUnit Test Patterns - Chapter19

How It Works(1)

● テストコードってどこに書くの?● ひとつひとつのテスト毎にメソッド (Test Method) にし

てクラスに配置しましょう

● How It Works● 各テストをメソッド/手続き/関数のかたちで Four-

Phase(358) の実装を行い、Fully Automated Test とする。

● 大事なのは、 assertion を書いて自己テストコード (Self-Checking Test:26) とすること

Page 5: xUnit Test Patterns - Chapter19

How It Works(2)

● Test Method には標準 Template がある● Simple Success Test

– 正常系のテスト。 Fixture setup から result verification まで一本道

● Expected Exception Test– 例外系のテスト

● Constructor Test– オブジェクトを作成し属性をテストするだけのテスト

Page 6: xUnit Test Patterns - Chapter19

Why We Do This

● 手続き型言語の場合● Test Method をファイルやモジュールに書く

● オブジェクト指向言語の場合● Test Method を Testcase Class(373) の中に書

き、Test Discovery(393) や Test Enumeration(399) を使って Test Method を Testcase Object(382) としてインスタンス化する

● 標準 template に従うことでテストを読みやすくシンプルにし、 SUT の動くドキュメントとすることができる

Page 7: xUnit Test Patterns - Chapter19

Implementation Notes● どう仕組みを実装する?

● Static method として実装し呼び出しを列挙する– テスト結果を集めたりする共通化がやりにくい

● Test Method 一つ一つに対応する Test Suite Object(387)をつくる– Test Discovery や Test Enumaration でインスタンス化する

場合に便利● 静的型付け言語の場合はメソッドに “throws

Exception” などを書かなければならない– コンパイラに対して「この例外は Test Runner が処理するよ」

という意思表示になる● 殆どの Test Method は3パターンに分類できる

Page 8: xUnit Test Patterns - Chapter19

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 が例外発生行を特定しやすくなること

Page 9: xUnit Test Patterns - Chapter19

      

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

Page 10: xUnit Test Patterns - Chapter19

      

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 しておけばよい

Page 11: xUnit Test Patterns - Chapter19

Expected Exception Test (1)● 多くの不具合は正常系以外のパスに潜む。特に例

外系のシナリオ。それは、● Untested Requirements (268) や、● Untested Code (268) であったりするため

● Expected Exception Test はわざと SUT が例外を出すようなテストを書き、きちんと例外が出ることを調べる● 例外の中身も調べたいときは Equality Assertion で調

べる● 例外が出なかったときは fail メソッドなどでテストを失

敗させる

Page 12: xUnit Test Patterns - Chapter19

Expected Exception Test (2)● 想定される (expected) 例外には、テストを書いた

ほうがよい● 再現が難しいが、出るかもしれない (might raise)

例外には、テストを書かなくてよい● (★ ネットワーク障害とか、 Disk full とか)● そういう例外は Simple Success Test の失敗として現

れるため● もしそういう例外もテストしたいなら、 Test Stub から例

外を発生させてテストする

Page 13: xUnit Test Patterns - Chapter19

Expected Exception Test (3)● 例外をテストするときの仕組み

● JUnit 3.x– ExpectedException クラスを継承させる (?)

● 小さいテストクラスが沢山できるし、あまり旨味は無い

● JUnit 4.x, NUnit– Test Method の annotation/attribute に書く

● Block のある言語 (Smalltalk, Ruby, …)– Block で例外が発生するか調べるテストを書ける

Page 14: xUnit Test Patterns - Chapter19

      

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 のダメな例

想定された例外なのにテストが失敗してしまう

Page 15: xUnit Test Patterns - Chapter19

      

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 させる

Page 16: xUnit Test Patterns - Chapter19

      

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 が投げる場合にはこう書くしかない !?

Page 17: xUnit Test Patterns - Chapter19

      

[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

Page 18: xUnit Test Patterns - Chapter19

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

Page 19: xUnit Test Patterns - Chapter19

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 でやってみる

Page 20: xUnit Test Patterns - Chapter19

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 の形をとる場合もある

Page 21: xUnit Test Patterns - Chapter19

Dependency Initialization Test● Constructor Test の亜種● 置き換え可能な依存があるオブジェクトがある場

合、本番環境では本物の依存オブジェクトが参照されることをテストする

● ふつうの Constructor Test と分けて管理した方がよい

Page 22: xUnit Test Patterns - Chapter19

   

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

Page 23: xUnit Test Patterns - Chapter19

   

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 正常系

Page 24: xUnit Test Patterns - Chapter19

   

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 異常系

Page 25: xUnit Test Patterns - Chapter19

   

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 は不要

Page 26: xUnit Test Patterns - Chapter19

Four-Phase Test

Page 27: xUnit Test Patterns - Chapter19

● 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

Page 28: xUnit Test Patterns - Chapter19

Four-Phase Test● 良いテストには4つの Phase がある

● Setup● Exercise● Verify● Teardown

● ワンパターン = テストの読みやすさ = Tests as Documentation● Test Method の中身はテスト内容に集中すべし

Page 29: xUnit Test Patterns - Chapter19

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 メソッド

Page 30: xUnit Test Patterns - Chapter19

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)

Page 31: xUnit Test Patterns - Chapter19

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)

Page 32: xUnit Test Patterns - Chapter19

Assertion Method

Page 33: xUnit Test Patterns - Chapter19

● 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

Page 34: xUnit Test Patterns - Chapter19

Assertion Method● どうやってテストに自己チェックさせるか

● ユーティリティメソッドを呼ぶことで望んだ結果になったかどうかの評価をすればいい

● Fully Automated Tests(26)の肝は、テストをSelf-Checking Tests(26)にすること● そのためには期待する結果を表現し、自動でチェックす

ることが必要

● Assertion Method は期待する結果を表現し、● コンピュータにとっては実行可能に● 人間には Tests as Documentation(23) になる。

Page 35: xUnit Test Patterns - Chapter19

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)でテス

トすることも可能

Page 36: xUnit Test Patterns - Chapter19

   

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

まずはひどい例から

Page 37: xUnit Test Patterns - Chapter19

   

/** * 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 はこうリファクタリングした

Page 38: xUnit Test Patterns - Chapter19

Implementation Notes● 全ての xUnit ファミリーは Assetion Method を備

えているが、考えるべきことはある● Assertion Method をどうやって呼ぶか● 最適な Assertion Method をどう選ぶか● Assertion Message(370) に何を書くか

Page 39: xUnit Test Patterns - Chapter19

Calling Built-in Assertion Methods● Test Method(348) からテストフレームワーク組み込みで提供されている Assertion Method を呼ぶには…● フレームワークが提供する Testcase Superclass(638)

を継承する (JUnit タイプ)● グローバルクラス/モジュールを完全修飾名で呼び出す

(NUnit タイプ)● Mixin (Test::Unit タイプ)● マクロ (CppUnit タイプ)

Page 40: xUnit Test Patterns - Chapter19

Assertion Messages● テスト失敗時の出力に含めるメッセージ

● どのテストが失敗したかをわかりやすくする– Assertion Roulette 参照

● 省略可能な引数として Assertion Method に渡すかたちが多い

● テスト失敗時に「なぜ失敗したか」の情報が多ければデバッグは容易になる● 正しい Assertion Method を選ぶことはエラー時のメッセージ出力を適切にする意味でも重要

● 問題なのは、 Assertion Message の引数の順番が xUnit 毎にブレていること

Page 41: xUnit Test Patterns - Chapter19

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

Page 42: xUnit Test Patterns - Chapter19

Equality Assertion● 結果が期待値と等価かどうかを調べる

● もっとも使われる Assertion Method

● 引数の順番は規約としては expected, actual の順番● 順番は失敗時のメッセージに関係するので重要● ★この順番でない xUnit もある。(NUnit とか)● ★自分の使う Equality Assertion の順を覚えよう

● 内部では等価性を調べるメソッドが呼ばれる● Java では equals とか● SUT ごと調べたい場合は Test-Specific Subclass

Page 43: xUnit Test Patterns - Chapter19

   

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 いろいろ

Page 44: xUnit Test Patterns - Chapter19

Fuzzy Equality Assertion

● 結果と期待値との完全な一致が難しい場合● 浮動小数点をあつかうとき● 期待値と完全一致させるには結果に本質的でない不要

なゴミが多いとき (XML の空白ノードとか)

assertEquals( 3.1415, diameter/2/radius, 0.001);

assertEquals( expectedXml, actualXml, elementsToCompare );

Page 45: xUnit Test Patterns - Chapter19

Stated Outcome Assertion

● 期待値を渡す必要がないとき● Conditional Test Logic を避けるためのガード節

としても使える

assertNotNull(a );

assertTrue(b > c );

assertNonZero(b );

Page 46: xUnit Test Patterns - Chapter19

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

Page 47: xUnit Test Patterns - Chapter19

Single-Outcome Assertion

● 常に同じ振る舞いをする Assertion Method● 例えば fail メソッド

● 使われる状況● まだ完成していないテスト Unfinished Test Assertion を示す● Expected Exception Test の中の try/catch ブロックで使う

fail( "Expected an exception" );

unfinishedTest();

Page 48: xUnit Test Patterns - Chapter19

      

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 させる

Page 49: xUnit Test Patterns - Chapter19

Assertion Message

Page 50: xUnit Test Patterns - Chapter19

● 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

Page 51: xUnit Test Patterns - Chapter19

Assertion Message● どの Assertion Method が落ちたか知りたい

● Assertion Method 毎にメッセージ引数を渡す

● テスト失敗時の出力に含めるメッセージ● どのテストが失敗したかをわかりやすくする

– Assertion Roulette 参照● 省略可能な引数として Assertion Method に渡すかた

ちが多い

Page 52: xUnit Test Patterns - Chapter19

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 を使いたくなる

Page 53: xUnit Test Patterns - Chapter19

Implementation Notes● Assertion Message に何を書くべきか

● Assertion-Identifying Message● Expectation-Describing Message● Argument-Describing Message

Page 54: xUnit Test Patterns - Chapter19

Assertion-Identifying Message● 同種の Assertion Method が複数ある場合にどれ

が失敗したか分かりにくい● Assertion Method 毎に違う文字列を渡して識別する

● 識別に使う文字列の例● Assertion Method に使う変数名

– 名前に悩む必要がないので便利かも● 単なる連番

– テストコードを読まないと結局どこが失敗したかわからなかったりする

Page 55: xUnit Test Patterns - Chapter19

Expectation-Describing Message● テストが失敗した時に「実際何が起こったか」はわ

かる。だが「何が起こるべきだったか」はわからない。● テストコード内にコメントを書く手もある

● もっと良いのは Assertion Message に期待値の説明を書くこと● Equality Assertion の場合は自動でやってくれるので必要無し

● Stated Outcome Assertion の場合は入れなければわからない

Page 56: xUnit Test Patterns - Chapter19

Argument-Describing Message● いくつかの Assertion Method は失敗時の出力が

不親切● 特に Stated Outcome Assertion

– assertTrue(式) とか– 失敗したのはわかるが、どんな式が失敗したのかわからない– 式を Assertion Message に含めてしまう手がある

Page 57: xUnit Test Patterns - Chapter19

      

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の例

Page 58: xUnit Test Patterns - Chapter19

ご清聴ありがとうございました