Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Spockで学ぶテスト駆動開発のコツ

 Spockで学ぶテスト駆動開発のコツ

JJUG CCC 2021 Springでの講演資料

yonetty

May 23, 2021
Tweet

More Decks by yonetty

Other Decks in Programming

Transcript

  1. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  6. JUnitじゃダメなの?

    View Slide

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

    View Slide

  8. テストコードは具体的であるため
    放っておくと散らかる
    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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  14. Spock超入門
    14

    View Slide

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

    View Slide

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

    View Slide

  17. 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で処理実行

    View Slide

  18. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  27. TDDを体得するには?
    27

    View Slide

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

    View Slide

  29. 29

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  40. 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

    View Slide

  41. 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

    View Slide

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

    View Slide

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

    View Slide

  44. 44

    View Slide

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

    View Slide

  46. 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

    View Slide

  47. 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

    View Slide

  48. 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

    View Slide

  49. // ユーザーストーリー関連の 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

    View Slide

  50. 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

    View Slide

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

    View Slide

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

    View Slide

  53. 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

    View Slide

  54. @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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  69. 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
    のラムダ式と異なる
    ジェネリック型のスタブ/
    モックは特殊な記述が必要

    View Slide

  70. - 以上 -
    70

    View Slide