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
良いユニットテストを書こう
Search
mototakatsu
December 20, 2024
Programming
4.4k
13
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
良いユニットテストを書こう
Ebisu.mobile #8 登壇資料です。
https://hey.connpass.com/event/335971/
mototakatsu
December 20, 2024
More Decks by mototakatsu
See All by mototakatsu
エンジニアと一緒にテストコードの設計と実装を改善した話
mototakatsu
0
280
Other Decks in Programming
See All in Programming
PHP に部分適用が来るぞ!……ところで何それ?おいしいの? #phpcon / phpcon-2026
shogogg
0
600
PostgreSQL 18で考えるUUID主キー
kazuhiro1982
0
460
わからない話を追いかけたら、プログラミング言語を作る側にいた
ydah
3
480
PHP初心者セッション2026 〜生成AIでは見えない裏側を知る:今だからLAMPを通して仕組みを学ぶ〜
kashioka
0
870
『コードを書く以外の』エンジニアリング〜課金基盤移行プロジェクト推進のためのTips4選
yuriko1211
0
590
複数の Claude Code が"放置"されてしまう問題をCLI ダッシュボードを自作して解決した話
sumihiro3
1
670
Claude CodeとAgentCore Gatewayを繋ぐ際の認証認可 / Authentication and authorization when connecting Claude Code with AgentCore Gateway
har1101
2
260
Android CLI
fornewid
0
210
Laravel Boostに学ぶ、AIにPHPを書かせる技術 〜OSSの実装から蒸留するエージェント制御の王道〜
kentaroutakeda
3
670
komatsuna「分散システムにおけるバグ分析手法」
komatsunaqa
0
240
生成AIで帳票OCRが「簡単に」作れる時代になった?
kon_shou
0
630
Detecting Compromised CI with eBPF and Cilium Tetragon
lizrice
0
160
Featured
See All Featured
RailsConf & Balkan Ruby 2019: The Past, Present, and Future of Rails at GitHub
eileencodes
141
35k
AI Search: Implications for SEO and How to Move Forward - #ShenzhenSEOConference
aleyda
1
1.3k
The MySQL Ecosystem @ GitHub 2015
samlambert
251
13k
Claude Code のすすめ
schroneko
67
230k
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
56
3.4k
DBのスキルで生き残る技術 - AI時代におけるテーブル設計の勘所
soudai
PRO
68
56k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
31
3.3k
Are puppies a ranking factor?
jonoalderson
1
3.8k
Six Lessons from altMBA
skipperchong
29
4.4k
Leveraging LLMs for student feedback in introductory data science courses - posit::conf(2025)
minecr
1
340
Why Our Code Smells
bkeepers
PRO
340
58k
Faster Mobile Websites
deanohume
310
32k
Transcript
© asken.inc 「良いユニットテスト」を書こう 2024/12/20 Ebisu.mobile #8 asken 高津 基暢
© asken.inc 2 自己紹介 株式会社asken Android Engineer / QA Engineer
Android Engineerとしてasken入社 Android開発は2012年から 元々テストが好きだったこともあって、 2024年からQA Engineer JSTQB FL保有 趣味はボードゲームと漫画とラジオ
© asken.inc 3 Agenda 1. なぜユニットテストを書く必要があるのか 2. 何を対象にユニットテストを書いているのか 3. 「良いユニットテスト」とは
4. 実際にどんな効果があったか
© asken.inc 4 1. なぜユニットテストを 書く必要があるのか
© asken.inc 5 Googleのテスト戦略 引用: https://developer.android.com/training/testing/fundamentals/strategies
© asken.inc 6 ユニットテストに期待される効果 • バグの早期発見 • リグレッションエラー(デグレ)の検出 • 設計改善
© asken.inc 7 効果を最大化するために • テストを書く範囲を決めて、カバレッジを上げる • 質が良いテストコードを書く
© asken.inc 8 2. あすけんAndroidでは何を対象に ユニットテストを書いているのか
© asken.inc 9 (前提)MVVM アーキテクチャを採用 Model, ViewModel(UiModel含む): ユニットテストを書いている View: ユニットテストを書いていない
コンポーネントテストを検討中 ユニットテストを書いているところ View ViewModel UiModel Model
© asken.inc 10 3. 「良いユニットテスト」と は 信頼性と可読性
© asken.inc 11 テストの結果が信頼できる 信頼できない = 実行するたびに結果が変わる(=不安定) テストの結果が不安定になる原因の例 • システム時刻を参照している
• データベースやSharedPreferencesを参照している
© asken.inc 12 テストの結果が信頼できる プロダクトコード: 正しい プロダクトコード: 誤り テスト結果: 成功
期待どおり 偽陰性 (検知漏れ) テスト結果: 失敗 偽陽性 (誤検知) 期待どおり 偽陽性、偽陰性のどちらも発生しない テストの結果が信頼できないと、 障害修正のコストが跳ね上がる!
© asken.inc 13 可読性を高めるために、以下の点を工夫する • 1つのテストケースで1つの観点をテストする • テストケース名でテストの意図がわかるようにする • AAAパターンまたはGherkin記法で整理する
• ループや条件分岐をなくし、上から下へ素直に読み下せるよ うにする • 文字列や数値をベタ書きする 可読性が高い
© asken.inc 14 読みにくいテストコード例 @Test fun validate_birthday() { val today
= LocalDateTime.now() val birthday = getDateBefore15Years(today) // 15歳の場合 (誕生日当日) は OK assertTrue(Birthday.validate(birthday, today)) // 14歳の場合 (誕生日前日) は NG … assertFalse(... } private fun getDateBefore15Years(today: LocalDateTime): LocalDateTime {
© asken.inc 15 NG: 1つのテストケースで複数テストしている @Test fun validate_birthday() { val
today = LocalDateTime.now() val birthday = getDateBefore15Years(today) // 15歳の場合 (誕生日当日) は OK assertTrue(Birthday.validate(birthday, today)) // 14歳の場合 (誕生日前日) は NG … assertFalse(... } private fun getDateBefore15Years(today: LocalDateTime): LocalDateTime { 複数の確認項目が1つのテストケースにある
© asken.inc 16 OK: 1つのテストケースで1つの観点をテストする @Test fun validate_birthday_正常系() { val
today = LocalDateTime.now() val birthday = getDateBefore15Years(today) // 15歳の場合 (誕生日当日) は OK assertTrue(Birthday.validate(birthday, today)) } @Test fun validate_birthday_異常系() { ... 確認項目ごとにテストケースを分割
© asken.inc 17 NG: 何を確認したいのかわからないテストケース名 @Test fun validate_birthday_正常系() { val
today = LocalDateTime.now() val birthday = getDateBefore15Years(today) // 15歳の場合 (誕生日当日) は OK assertTrue(Birthday.validate(birthday, today)) } private fun getDateBefore15Years(today: LocalDateTime): LocalDateTime { ... 正常って何? 何がどうなっていれば正しい? テスト対象の関数名が テストケース名に含まれている
© asken.inc 18 OK: テストケース名でテストの意図がわかる @Test fun 15歳の誕生日当日は登録可能とする() { val
today = LocalDateTime.now() val birthday = getDateBefore15Years(today) assertTrue(Birthday.validate(birthday, today)) } private fun getDateBefore15Years(today: LocalDateTime): LocalDateTime { ...
© asken.inc 19 NG: テストケースの処理フローの説明がない @Test fun 15歳の誕生日当日は登録可能とする() { val
today = LocalDateTime.now() val birthday = getDateBefore15Years(today) assertTrue(Birthday.validate(birthday, today)) } private fun getDateBefore15Years(today: LocalDateTime): LocalDateTime { ... AAAパターンのコメントを追加して テストの流れを整理する
© asken.inc 20 OK: AAAパターンで処理フローを整理する @Test fun 15歳の誕生日当日は登録可能とする() { //
Arrange // 15歳の誕生日を用意する val today = LocalDateTime.now() val birthday = getDateBefore15Years(today) // Act val actual = Birthday.validate(birthday, today) // Assert assertTrue(actual) } private fun getDateBefore15Years(today: LocalDateTime): LocalDateTime { Arrange / Act / Assert に分割して整理する
© asken.inc 21 NG: テストデータ作成を関数化している @Test fun 15歳の誕生日当日は登録可能とする() { //
Arrange // 15歳の誕生日を用意する val today = LocalDateTime.now() val birthday = getDateBefore15Years(today) // Act val actual = Birthday.validate(birthday, today) // Assert assertTrue(actual) } private fun getDateBefore15Years(today: LocalDateTime): LocalDateTime { 他のテストケースでも同じ処理を使って いるので関数化している
© asken.inc 22 OK: 上から下へ素直に読み下せるようにする @Test fun 15歳の誕生日当日は登録可能とする() { //
Arrange // 15歳の誕生日を用意する val today = LocalDateTime.now() val birthday = today.minusYears(15) // Act val actual = Birthday.validate(birthday, today) // Assert assertTrue(actual) } 他のテストケースと同じ処理であっても 関数の切り出しをしない
© asken.inc 23 NG: テストデータを計算している @Test fun 15歳の誕生日当日は登録可能とする() { //
Arrange // 15歳の誕生日を用意する val today = LocalDateTime.now() val birthday = today.minusYears(15) // Act val actual = Birthday.validate(birthday, today) // Assert assertTrue(actual) } システム時刻から今日が15歳の誕生日とな る日付を計算している
© asken.inc 24 OK: テストデータに必要な値をベタ書きする @Test fun 15歳の誕生日当日は登録可能とする() { //
Arrange // 今日と15歳の誕生日を用意する val today = SimpleDateFormat("yyyyMMdd").parse("20241201") val birthday = SimpleDateFormat("yyyyMMdd").parse("20091201") // Act val actual = Birthday.validate(birthday, today) // Assert assertTrue(actual) } “今日”の日付と、その日が15歳の誕生日と なる日をベタ書きする
© asken.inc 25 4. 実際にどんな効果があったか
© asken.inc 26 askenエンジニアが実感している効果 期待される効果を実際に体験できている • バグの早期発見 • リグレッションエラー(デグレ)の検出 •
設計改善
© asken.inc 27 予想外の効果 iOSエンジニアにテストコードの レビューをしてもらえるようになった これまでの対応によりKotlinに不慣れでも テストコードの内容は理解できる レビューしてもらった結果… iOS
- Android間の仕様差異を事前に検出できた!!
© asken.inc 28 We are hiring! askenで一緒に活躍してくれる方を募集しています!
© asken.inc 29 Thank you!