Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Spockで学ぶテスト駆動開発のコツ
Search
yonetty
May 23, 2021
Programming
1
940
Spockで学ぶテスト駆動開発のコツ
JJUG CCC 2021 Springでの講演資料
yonetty
May 23, 2021
Tweet
Share
More Decks by yonetty
See All by yonetty
技術書を読む技術(JJUG CCC 2023 Fall)
yonetty
3
590
『アジャイルソフトウェア開発の奥義』から学ぶテスト駆動開発
yonetty
0
350
Testing Java With Spock
yonetty
0
68
Other Decks in Programming
See All in Programming
CSC509 Lecture 08
javiergs
PRO
0
110
カスタムしながら理解するGraphQL Connection
yanagii
1
1.2k
【Kaigi on Rails 2024】YOUTRUST スポンサーLT
krpk1900
1
250
『ドメイン駆動設計をはじめよう』のモデリングアプローチ
masuda220
PRO
8
440
go.mod、DockerfileやCI設定に分散しがちなGoのバージョンをまとめて管理する / Go Connect #3
arthur1
10
2.4k
生成 AI を活用した toitta 切片分類機能の裏側 / Inside toitta's AI-Based Factoid Clustering
pokutuna
0
580
Piniaの現状と今後
waka292
5
1.5k
LLM生成文章の精度評価自動化とプロンプトチューニングの効率化について
layerx
PRO
2
140
Why Spring Matters to Jakarta EE - and Vice Versa
ivargrimstad
0
980
Server Driven Compose With Firebase
skydoves
0
400
Googleのテストサイズを活用したテスト環境の構築
toms74209200
0
270
AWS IaCの注目アップデート 2024年10月版
konokenj
3
3.1k
Featured
See All Featured
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
328
21k
How to train your dragon (web standard)
notwaldorf
88
5.7k
Reflections from 52 weeks, 52 projects
jeffersonlam
346
20k
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
92
16k
Being A Developer After 40
akosma
86
590k
BBQ
matthewcrist
85
9.3k
Navigating Team Friction
lara
183
14k
Side Projects
sachag
452
42k
Designing for Performance
lara
604
68k
Practical Orchestrator
shlominoach
186
10k
Making Projects Easy
brettharned
115
5.9k
Automating Front-end Workflow
addyosmani
1365
200k
Transcript
None
米久保 剛 (Takeshi Yonekubo) 株式会社電通国際情報サービス 所属 自社パッケージ製品の開発リーダー twitter: @tyonekubo 2
3 1. Spockの紹介 2. テスト駆動開発のコツ 3. テストコードの保守性向上
1. テストの書き方がよくわからない 2. テストを書くのが面倒くさい 3. テストしにくい 4. テストコードのメンテナンスが大変 4
✓ Groovy製のテスティング・フレームワーク ✓ Javaとの相互運用性の高さ ✓ Groovyの文法 + DSLで楽にテストを書ける ✓ 強力なアサート機能
✓ 機能が豊富なのでSpock単体で広くカバー可能 ✓ モック機能 ✓ Springとの統合
JUnitじゃダメなの?
1. テストの書き方がよくわからない 2. テストを書くのが面倒くさい 3. テストしにくい 4. テストコードのメンテナンスが大変 7
テストコードは具体的であるため 放っておくと散らかる 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
テストコードの量は プロダクトコードの量の数倍になる と言われている 9
少しでもテストコードを楽に書きたい 10
result == ["cat", "dog"] // Listのリテラル表記 // [cat: 3, dog:
4] (Mapリテラル) ListやMapなどのコレクション リテラルがあるので、簡単に書ける == による等価判定(Javaのeqauals) 11
where: unitPrice | quantity || expectedAmount 100 | 0 ||
0 0 | 5 || 0 100 | 1 || 100 100 | 2 || 200 テストの入力値や期待値のセット を表形式で簡潔に定義できる (パラメータ化テスト) 12
1. テストの書き方がよくわからない 2. テストを書くのが面倒くさい 3. テストしにくい 4. テストコードのメンテナンスが大変 13
Spock超入門 14
1. Setup 前処理 テストの事前条件(テストフィクスチャ)をそろえる 2. Exercise 実行 テスト対象処理(SUT)を呼び出す 3. Verify
検証 実行結果(事後条件)が期待通りかを確認する 4. Teardown 後処理 必要な場合、後始末を行う 15
class SampleSpec extends Specification { } spock.lang.Specification を継承 16 テストクラス名は
~Spec が慣習
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で処理実行
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
def “アサーションのサンプル"() { when: def list = ["cat", "dog"] then:
list // 等式でなくても、Truthy評価される。 // リストの場合、nullでなく要素が1つ以上あれば真 list.size() == 2 // プリミティブの等価性比較 list == ["cat", "dog"] // オブジェクトの等価性比較 } thenブロックは特別扱いされる。 真と評価されるべき文を列挙して検証を行う。 if文やfor文などの制御構文は 入れられないので注意 19
まずはこれだけ覚えれば 最初のSpockテストはすぐ書ける! 20
21 1. テストの書き方がよくわからない 2. テストを書くのが面倒くさい 3. テストしにくい 4. テストコードのメンテナンスが大変
テストしにくい =テスト容易性が低い 22
テストを先に書くことで、プロダクト コードは自然とテストしやすいコードに なる(テストファースト) 23
テスト駆動開発 Test-Driven Development 24
テスト駆動開発(TDD): テストファーストの手法を使ってソフト ウェアを漸進的に開発していくプロセス をオートマティックにするやり方 25
①失敗するテストを書く ②テストが通るよう にコードを書く ③リファクタリングする 呼吸するかの如く自然にサイクルを回すことが大事 TDD 26
TDDを体得するには? 27
- ひたすら鍛錬あるのみ 28
29
達人の指南を受ける 技を盗む 30
TDDを実践する上でのコツ 31
TDDのコツ① ユニットテストの単位 32
クラス単位ではなく、振舞いを提供する コンポーネント単位でテストを書く 33
✓ テスト駆動で漸進的にコンポーネントの実装を進 める ✓ コンポーネントが、単一クラスとなるか集合体と なるかは結果論 ✓ コンポーネントをブラックボックスとして捉えて テストを書けば、カバレッジは自然と担保される 6章のボーリングスコア計算アプリの例
34
“ユニットとはふるまいの 単位、つまり独立した検証 可能なふるまいのことだ” “あらゆる観察可能なふる まいが、それに紐づくテス トを持つべきである” 35
✓ テスト対象クラスを他と独立して検証するために、 スタブやモックを多用することになる ✓ 結果として、脆いテスト(Fragile Test)に なりやすい(内部設計の変更でテストが壊れる) 36
TDDのコツ② スタブ・モックの使いどころ 37
コンポーネント(SUT) 依存コンポーネント(DOC) コンポーネント(SUT) スタブ ✓ 他のコンポーネント(ふるまいの集合体)を スタブに置き換えてテスト ✓ DOCの呼び出され方(相互作用)は検証しない ※DOC
…Depended-on Component 38
コンポーネント(SUT) 外部サービス コンポーネント(SUT) モック ✓ 外部サービスをモックに置き換えてテストする (サービスの例:メール送信、API呼出し) ✓ 外部サービスが呼び出されることが仕様の一部 なので、モック機能によって相互作用を検証する
39
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
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
42 1. テストの書き方がよくわからない 2. テストを書くのが面倒くさい 3. テストしにくい 4. テストコードのメンテナンスが大変
テストコードは具体的であるため 放っておくと散らかる 43
44
整理整頓テクニック① 複雑なオブジェクトの生成パターン 45
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
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
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
// ユーザーストーリー関連の 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
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
Test Data Builder Object Mother Pros ✓ 柔軟性が高い ✓ テストデータの生成ロジックを
1箇所にまとめられる ✓ 再利用性が高い ✓ テストコードの可読性が高くなる Cons ✓ 再利用性は高くない ✓ テストコードの冗長さは残る ✓ ファクトリメソッドが増えて 神クラスになりがち ✓ 複数のテストがObject Motherに 依存することになる ※どちらも複数のテストクラスで利用するデータ生成問題を解決するために 使用する(単一テストクラスであればprivateな生成メソッドで十分) 51
整理整頓テクニック② パラメータ化テストパターン 52
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
@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
55 1. テストの書き方がよくわからない 2. テストを書くのが面倒くさい 3. テストしにくい 4. テストコードのメンテナンスが大変
✓ テストコードも大切なプロジェクト資産 56 ✓ TDDやテストパターンを活用しプロダクト コードもテストコードも保守性を高めよう ✓ Spockはとにかく使いやすいので、使って みてください!
ご清聴ありがとうございました 発表資料や補足情報などはtwitterで発信します @tyonekubo 57
【補足】 Spock導入時のコツ 58
Q. JUnitより遅くないの? 59
A. 単純比較だと遅いけど、使い方次第 60 ✓ JUnitでもテストスイートが大きくにつれ遅くなる ✓ TDDで回すときはサブモジュールやパッケージ限定で実行 ✓ 最後に全テストスイートを実行
Q. メンバーがGroovy/Spockをすぐに 使いこなせるか不安です 61
A. テストコードを書くために 覚える量は多くない 62 ✓ 軽量なガイドラインとサンプルコードがあればOK
Groovyの基本文法を 含む十数ページ相当の ガイドラインで、すぐ に皆が使い方を覚えた。
Zenn Bookに移植した ので参考にしてください https://zenn.dev/yonekubo/books/6f4bde620a7bac
Q. IDEとの統合は問題ないか? 65
A. Eclipse < IntelliJ(個人の感想) 66 ✓ Eclipseのプラグインはあるが、難点がありIntelliJを薦める ✓ Eclipseだと、テストメソッド単位での実行ができない ✓
Eclipseだと、テストスイートのロードに時間がかかる
Q. ハマることはないの? 67
A. 稀にあります 68 ✓ 過去にハマった例: ✓ Javaのラムダ式に対応するのはクロージャだが、記法に差 異があるため初歩的ミスに気がつけなかった ✓ ジェネリックなクラス/インタフェースのスタブ/モック
方法が難しい(特殊な記述が必要)
def “クロージャ”() { given: “Fake” // Function<String, String> 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<Function<String, String>>(){}.type) stub.apply("baz") >> "BAZ" and: "SUT" def sut = new SUT3(stub) when: "実行" def result = sut.foo("baz") then: "結果が正しい" result == "BAZBAZ" } 69 クロージャの記法はJava のラムダ式と異なる ジェネリック型のスタブ/ モックは特殊な記述が必要
- 以上 - 70