Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

米久保 剛 (Takeshi Yonekubo) 株式会社電通国際情報サービス 所属 自社パッケージ製品の開発リーダー twitter: @tyonekubo 2

Slide 3

Slide 3 text

3 1. Spockの紹介 2. テスト駆動開発のコツ 3. テストコードの保守性向上

Slide 4

Slide 4 text

1. テストの書き方がよくわからない 2. テストを書くのが面倒くさい 3. テストしにくい 4. テストコードのメンテナンスが大変 4

Slide 5

Slide 5 text

✓ Groovy製のテスティング・フレームワーク ✓ Javaとの相互運用性の高さ ✓ Groovyの文法 + DSLで楽にテストを書ける ✓ 強力なアサート機能 ✓ 機能が豊富なのでSpock単体で広くカバー可能 ✓ モック機能 ✓ Springとの統合

Slide 6

Slide 6 text

JUnitじゃダメなの?

Slide 7

Slide 7 text

1. テストの書き方がよくわからない 2. テストを書くのが面倒くさい 3. テストしにくい 4. テストコードのメンテナンスが大変 7

Slide 8

Slide 8 text

テストコードは具体的であるため 放っておくと散らかる public static double avg(double... numbers) { if (numbers.length == 0) { throw new IllegalArgumentException(); } double sum = 0; for (double d: numbers) { sum += d; } return sum / numbers.length; } {} //空の配列 {1.0} //要素1つ {1.0, 2.3} //要素2つ {1.0, 2.3, 3.4} //要素3つ … プロダクトコード テストコード 引数の数を変えて具体値で テストケースを記述する 8

Slide 9

Slide 9 text

テストコードの量は プロダクトコードの量の数倍になる と言われている 9

Slide 10

Slide 10 text

少しでもテストコードを楽に書きたい 10

Slide 11

Slide 11 text

result == ["cat", "dog"] // Listのリテラル表記 // [cat: 3, dog: 4] (Mapリテラル) ListやMapなどのコレクション リテラルがあるので、簡単に書ける == による等価判定(Javaのeqauals) 11

Slide 12

Slide 12 text

where: unitPrice | quantity || expectedAmount 100 | 0 || 0 0 | 5 || 0 100 | 1 || 100 100 | 2 || 200 テストの入力値や期待値のセット を表形式で簡潔に定義できる (パラメータ化テスト) 12

Slide 13

Slide 13 text

1. テストの書き方がよくわからない 2. テストを書くのが面倒くさい 3. テストしにくい 4. テストコードのメンテナンスが大変 13

Slide 14

Slide 14 text

Spock超入門 14

Slide 15

Slide 15 text

1. Setup 前処理 テストの事前条件(テストフィクスチャ)をそろえる 2. Exercise 実行 テスト対象処理(SUT)を呼び出す 3. Verify 検証 実行結果(事後条件)が期待通りかを確認する 4. Teardown 後処理 必要な場合、後始末を行う 15

Slide 16

Slide 16 text

class SampleSpec extends Specification { } spock.lang.Specification を継承 16 テストクラス名は ~Spec が慣習

Slide 17

Slide 17 text

class SampleSpec extends Specification { def "引数が2つの場合に平均値を取得できる"() { given: "引数が2つの配列がある" def numbers = [1.2, 1.8] as double[] when: "平均値を求める" def avg = Sample.avg(numbers); then: "平均値が正しい" avg == 1.5 } } ブロックラベル: BDDスタイルの given-when-then がおすすめ 17 文字列をメソッド名に できるのでテスト ケース名を記述 givenには 前処理を記述 thenにて結果検証 (アサーション) whenで処理実行

Slide 18

Slide 18 text

class SampleSpec extends Specification { def setup() { // テストケース毎に実行される共通の前処理 } def "引数が2つの場合に平均値を取得できる"() { given: "引数が2つの配列がある" def numbers = [1.2, 1.8] as double[] when: "平均値を求める" def avg = Sample.avg(numbers); then: "平均値が正しい" avg == 1.5 } def cleanup() { // テストケース毎に実行される共通の後処理 } } 各テストケースに 共通の前処理は setupメソッド に記述 各テストケースに 共通の後処理は cleanupメソッド に記述 18

Slide 19

Slide 19 text

def “アサーションのサンプル"() { when: def list = ["cat", "dog"] then: list // 等式でなくても、Truthy評価される。 // リストの場合、nullでなく要素が1つ以上あれば真 list.size() == 2 // プリミティブの等価性比較 list == ["cat", "dog"] // オブジェクトの等価性比較 } thenブロックは特別扱いされる。 真と評価されるべき文を列挙して検証を行う。 if文やfor文などの制御構文は 入れられないので注意 19

Slide 20

Slide 20 text

まずはこれだけ覚えれば 最初のSpockテストはすぐ書ける! 20

Slide 21

Slide 21 text

21 1. テストの書き方がよくわからない 2. テストを書くのが面倒くさい 3. テストしにくい 4. テストコードのメンテナンスが大変

Slide 22

Slide 22 text

テストしにくい =テスト容易性が低い 22

Slide 23

Slide 23 text

テストを先に書くことで、プロダクト コードは自然とテストしやすいコードに なる(テストファースト) 23

Slide 24

Slide 24 text

テスト駆動開発 Test-Driven Development 24

Slide 25

Slide 25 text

テスト駆動開発(TDD): テストファーストの手法を使ってソフト ウェアを漸進的に開発していくプロセス をオートマティックにするやり方 25

Slide 26

Slide 26 text

①失敗するテストを書く ②テストが通るよう にコードを書く ③リファクタリングする 呼吸するかの如く自然にサイクルを回すことが大事 TDD 26

Slide 27

Slide 27 text

TDDを体得するには? 27

Slide 28

Slide 28 text

- ひたすら鍛錬あるのみ 28

Slide 29

Slide 29 text

29

Slide 30

Slide 30 text

達人の指南を受ける 技を盗む 30

Slide 31

Slide 31 text

TDDを実践する上でのコツ 31

Slide 32

Slide 32 text

TDDのコツ① ユニットテストの単位 32

Slide 33

Slide 33 text

クラス単位ではなく、振舞いを提供する コンポーネント単位でテストを書く 33

Slide 34

Slide 34 text

✓ テスト駆動で漸進的にコンポーネントの実装を進 める ✓ コンポーネントが、単一クラスとなるか集合体と なるかは結果論 ✓ コンポーネントをブラックボックスとして捉えて テストを書けば、カバレッジは自然と担保される 6章のボーリングスコア計算アプリの例 34

Slide 35

Slide 35 text

“ユニットとはふるまいの 単位、つまり独立した検証 可能なふるまいのことだ” “あらゆる観察可能なふる まいが、それに紐づくテス トを持つべきである” 35

Slide 36

Slide 36 text

✓ テスト対象クラスを他と独立して検証するために、 スタブやモックを多用することになる ✓ 結果として、脆いテスト(Fragile Test)に なりやすい(内部設計の変更でテストが壊れる) 36

Slide 37

Slide 37 text

TDDのコツ② スタブ・モックの使いどころ 37

Slide 38

Slide 38 text

コンポーネント(SUT) 依存コンポーネント(DOC) コンポーネント(SUT) スタブ ✓ 他のコンポーネント(ふるまいの集合体)を スタブに置き換えてテスト ✓ DOCの呼び出され方(相互作用)は検証しない ※DOC …Depended-on Component 38

Slide 39

Slide 39 text

コンポーネント(SUT) 外部サービス コンポーネント(SUT) モック ✓ 外部サービスをモックに置き換えてテストする (サービスの例:メール送信、API呼出し) ✓ 外部サービスが呼び出されることが仕様の一部 なので、モック機能によって相互作用を検証する 39

Slide 40

Slide 40 text

def "スタブの利用例"() { given: "スタブのセットアップ" def stubDoc = Stub(DOC) stubDoc.bar("baz") >> "BAZ" // stubDoc.bar(_) >> "BAZ" // ワイルドカード利用例 and: "SUT" def sut = new SUT1(stubDoc) // スタブを注入 when: "実行" def result = sut.foo("baz") then: "結果が正しい" result == "BAZBAZ" } Stub(クラス名/インタフェース名) でスタブ作成 スタブの振舞いを DSLで簡潔に記述 40

Slide 41

Slide 41 text

def "モックの利用例"() { given: "モックの作成" def mockService = Mock(Service) and: "SUT" def sut = new SUT2(mockService) when: "実行" def result = sut.foo("baz") then: "結果が正しい" result == "BAZBAZ" and: "サービスとの相互作用が正しい" 1 * mockService.bar("baz") >> "BAZ" } Mock(クラス名/インタフェース名) でスタブ作成 期待されるモック呼出しを DSLで簡潔に記述 41

Slide 42

Slide 42 text

42 1. テストの書き方がよくわからない 2. テストを書くのが面倒くさい 3. テストしにくい 4. テストコードのメンテナンスが大変

Slide 43

Slide 43 text

テストコードは具体的であるため 放っておくと散らかる 43

Slide 44

Slide 44 text

44

Slide 45

Slide 45 text

整理整頓テクニック① 複雑なオブジェクトの生成パターン 45

Slide 46

Slide 46 text

def "タスクが全て完了となったらストーリーも完了する"() { given: "ストーリーがある" def story = new UserStory("最初のユニットテストを書く") story.estimate(2) and: "1つ目のタスク(完了)" def task1 = new Task("Spockをインストールする") def alice = new User("Alice", "[email protected]") task1.assign(alice) story.addTask(task1) task1.finish() and: "2つ目のタスク(着手)" def task2 = new Task("CIに組み込む") def bob = new User("Bob", "[email protected]") task2.assign(bob) story.addTask(task2) task2.start() when: "2つ目のタスクを完了する" task2.finish() then: "タスクが完了" task2.status == Status.Completed and: "ストーリーも完了" story.status == Status.Completed } テスト対象オブジェ クトの生成処理が 大半を占める 46

Slide 47

Slide 47 text

def "タスクが全て完了となったらストーリーも完了する_TestDataBuilder"() { given: "ストーリーがある" def builder = new UserStoryBuilder() UserStory story = builder.story(name: "最初のユニットテストを書く", point: 2) { task(name: "Spockをインストールする") { assignee(userId: "Alice", email: "[email protected]") finish() } task(name: "CIに組み込む") { assignee(userId: "Bob", email: "[email protected]") } } when: "2つ目のタスクを完了する" def task2 = story.getTask("Spockをインストールする") task2.finish() … } 一種のDSLにより構造化 してテストデータを記述 できるので把握しやすい 47

Slide 48

Slide 48 text

class UserStoryBuilder extends FactoryBuilderSupport { UserStoryBuilder() { registerFactory("story", new UserStoryFactory()) registerFactory("task", new TaskFactory()) registerFactory("finish", new TaskFinishFactory()) registerFactory("assignee", new UserFactory()) } } class UserStoryFactory extends AbstractFactory { @Override Object newInstance(FactoryBuilderSupport builder, Object name, Object value, Map attributes) throws InstantiationException, IllegalAccessException { def storyName = attributes.get("name", "Some Story") def story = new UserStory(storyName) def point = attributes.get("point", 1) story.estimate(point) story } @Override void setChild(FactoryBuilderSupport builder, Object parent, Object child) { def story = parent as UserStory def task = child as Task story.addTask(task) } } Groovy標準ライブラリを 利用してBuilderを実装 (説明は割愛) 48

Slide 49

Slide 49 text

// ユーザーストーリー関連の Object Mother class ObjectMother { // 完了タスクを1つもつユーザーストーリーを生成 static def aStoryWithATaskFinished(name = "Some Story") { def story = new UserStory(name) def task = aTask() story.addTask(task) task.finish() story } // タスクを生成 static def aTask(name = "Some Task") { def task = new Task(name) def user = new User("Someone", "[email protected]") task.assign(user) task } } • テストデータのファクトリメソッドの集合 • テストデータをパターン化し、意図が明確な名前を与える 49

Slide 50

Slide 50 text

def "タスクが全て完了となったらストーリーも完了する_ObjectMother"() { given: "ストーリーがある" def story = ObjectMother.aStoryWithATaskFinished() and: "2つ目のタスクを追加して開始" def task2 = ObjectMother.aTask() story.addTask(task2) task2.start() when: "2つ目のタスクを完了する" task2.finish() then: "タスクが完了" task2.status == Status.Completed and: "ストーリーも完了" story.status == Status.Completed } コード量削減に加え、意図 が明確な名前によりテスト の事前条件を把握しやすい 別のテクニックとして、 テスト対象の振舞いに影響 を与えない(ストーリー名 などの)情報は省略する 50

Slide 51

Slide 51 text

Test Data Builder Object Mother Pros ✓ 柔軟性が高い ✓ テストデータの生成ロジックを 1箇所にまとめられる ✓ 再利用性が高い ✓ テストコードの可読性が高くなる Cons ✓ 再利用性は高くない ✓ テストコードの冗長さは残る ✓ ファクトリメソッドが増えて 神クラスになりがち ✓ 複数のテストがObject Motherに 依存することになる ※どちらも複数のテストクラスで利用するデータ生成問題を解決するために 使用する(単一テストクラスであればprivateな生成メソッドで十分) 51

Slide 52

Slide 52 text

整理整頓テクニック② パラメータ化テストパターン 52

Slide 53

Slide 53 text

def "定額割引クーポンの値引き計算が正しい 金額>クーポン値引き額"() { given: def sut = aFixedAmountCoupon(300) when: def discounted = sut.discount(BigDecimal.valueOf(500)) then: discounted == 300 } def "定額割引クーポンの値引き計算が正しい 金額=クーポン値引き額"() { given: def sut = aFixedAmountCoupon(300) when: def discounted = sut.discount(BigDecimal.valueOf(300)) then: discounted == 300 } def "定額割引クーポンの値引き計算が正しい 金額<クーポン値引き額"() { given: def sut = aFixedAmountCoupon(300) when: def discounted = sut.discount(BigDecimal.valueOf(299)) then: discounted == 299 } 事前条件、テスト対象の 振舞い、検証内容が似通っ たテストケースが複数存在 問題点: • テストコードが冗長 • 網羅性が把握しにくい 53

Slide 54

Slide 54 text

@Unroll def "Discount calculation for FixedAmountCoupon(300), Amount: #amount Expected: #expected"() { given: def sut = aFixedAmountCoupon(300) when: def discounted = sut.discount(BigDecimal.valueOf(amount)) then: discounted == BigDecimal.valueOf(expected) where: amount | expected || description 500 | 300 || "金額 > クーポン値引き額" 300 | 300 || "金額 = クーポン値引き額" 299 | 299 || "金額 < クーポン値引き額" } whereブロックに表形式で 記述した1行1行がテスト ケースとして実行される whereブロックの列ヘッダ 名を変数として参照できる 実行結果もわかりやすい 54

Slide 55

Slide 55 text

55 1. テストの書き方がよくわからない 2. テストを書くのが面倒くさい 3. テストしにくい 4. テストコードのメンテナンスが大変

Slide 56

Slide 56 text

✓ テストコードも大切なプロジェクト資産 56 ✓ TDDやテストパターンを活用しプロダクト コードもテストコードも保守性を高めよう ✓ Spockはとにかく使いやすいので、使って みてください!

Slide 57

Slide 57 text

ご清聴ありがとうございました 発表資料や補足情報などはtwitterで発信します @tyonekubo 57

Slide 58

Slide 58 text

【補足】 Spock導入時のコツ 58

Slide 59

Slide 59 text

Q. JUnitより遅くないの? 59

Slide 60

Slide 60 text

A. 単純比較だと遅いけど、使い方次第 60 ✓ JUnitでもテストスイートが大きくにつれ遅くなる ✓ TDDで回すときはサブモジュールやパッケージ限定で実行 ✓ 最後に全テストスイートを実行

Slide 61

Slide 61 text

Q. メンバーがGroovy/Spockをすぐに 使いこなせるか不安です 61

Slide 62

Slide 62 text

A. テストコードを書くために 覚える量は多くない 62 ✓ 軽量なガイドラインとサンプルコードがあればOK

Slide 63

Slide 63 text

Groovyの基本文法を 含む十数ページ相当の ガイドラインで、すぐ に皆が使い方を覚えた。

Slide 64

Slide 64 text

Zenn Bookに移植した ので参考にしてください https://zenn.dev/yonekubo/books/6f4bde620a7bac

Slide 65

Slide 65 text

Q. IDEとの統合は問題ないか? 65

Slide 66

Slide 66 text

A. Eclipse < IntelliJ(個人の感想) 66 ✓ Eclipseのプラグインはあるが、難点がありIntelliJを薦める ✓ Eclipseだと、テストメソッド単位での実行ができない ✓ Eclipseだと、テストスイートのロードに時間がかかる

Slide 67

Slide 67 text

Q. ハマることはないの? 67

Slide 68

Slide 68 text

A. 稀にあります 68 ✓ 過去にハマった例: ✓ Javaのラムダ式に対応するのはクロージャだが、記法に差 異があるため初歩的ミスに気がつけなかった ✓ ジェネリックなクラス/インタフェースのスタブ/モック 方法が難しい(特殊な記述が必要)

Slide 69

Slide 69 text

def “クロージャ”() { given: “Fake” // Function def fake = { arg -> “BAZ”} // def fake = { “BAZ” } // 暗黙のitを捨ててこうも書ける and: "SUT" def sut = new SUT3(fake) when: "実行" def result = sut.foo("baz") then: "結果が正しい" result == "BAZBAZ" } def "ジェネリック"() { given: "Stub" def stub = Stub(type: new TypeToken>(){}.type) stub.apply("baz") >> "BAZ" and: "SUT" def sut = new SUT3(stub) when: "実行" def result = sut.foo("baz") then: "結果が正しい" result == "BAZBAZ" } 69 クロージャの記法はJava のラムダ式と異なる ジェネリック型のスタブ/ モックは特殊な記述が必要

Slide 70

Slide 70 text

- 以上 - 70