Slide 1

Slide 1 text

© asken.inc 「良いユニットテスト」を書こう 2024/12/20 Ebisu.mobile #8 asken 高津 基暢

Slide 2

Slide 2 text

© asken.inc 2 自己紹介 株式会社asken Android Engineer / QA Engineer Android Engineerとしてasken入社 Android開発は2012年から 元々テストが好きだったこともあって、 2024年からQA Engineer JSTQB FL保有 趣味はボードゲームと漫画とラジオ

Slide 3

Slide 3 text

© asken.inc 3 Agenda 1. なぜユニットテストを書く必要があるのか 2. 何を対象にユニットテストを書いているのか 3. 「良いユニットテスト」とは 4. 実際にどんな効果があったか

Slide 4

Slide 4 text

© asken.inc 4 1. なぜユニットテストを 書く必要があるのか

Slide 5

Slide 5 text

© asken.inc 5 Googleのテスト戦略 引用: https://developer.android.com/training/testing/fundamentals/strategies

Slide 6

Slide 6 text

© asken.inc 6 ユニットテストに期待される効果 ● バグの早期発見 ● リグレッションエラー(デグレ)の検出 ● 設計改善

Slide 7

Slide 7 text

© asken.inc 7 効果を最大化するために ● テストを書く範囲を決めて、カバレッジを上げる ● 質が良いテストコードを書く

Slide 8

Slide 8 text

© asken.inc 8 2. あすけんAndroidでは何を対象に ユニットテストを書いているのか

Slide 9

Slide 9 text

© asken.inc 9 (前提)MVVM アーキテクチャを採用 Model, ViewModel(UiModel含む): ユニットテストを書いている View: ユニットテストを書いていない コンポーネントテストを検討中 ユニットテストを書いているところ View ViewModel UiModel Model

Slide 10

Slide 10 text

© asken.inc 10 3. 「良いユニットテスト」と は 信頼性と可読性

Slide 11

Slide 11 text

© asken.inc 11 テストの結果が信頼できる 信頼できない = 実行するたびに結果が変わる(=不安定) テストの結果が不安定になる原因の例 ● システム時刻を参照している ● データベースやSharedPreferencesを参照している

Slide 12

Slide 12 text

© asken.inc 12 テストの結果が信頼できる プロダクトコード: 正しい プロダクトコード: 誤り テスト結果: 成功 期待どおり 偽陰性 (検知漏れ) テスト結果: 失敗 偽陽性 (誤検知) 期待どおり 偽陽性、偽陰性のどちらも発生しない テストの結果が信頼できないと、 障害修正のコストが跳ね上がる!

Slide 13

Slide 13 text

© asken.inc 13 可読性を高めるために、以下の点を工夫する ● 1つのテストケースで1つの観点をテストする ● テストケース名でテストの意図がわかるようにする ● AAAパターンまたはGherkin記法で整理する ● ループや条件分岐をなくし、上から下へ素直に読み下せるよ うにする ● 文字列や数値をベタ書きする 可読性が高い

Slide 14

Slide 14 text

© 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 {

Slide 15

Slide 15 text

© 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つのテストケースにある

Slide 16

Slide 16 text

© 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_異常系() { ... 確認項目ごとにテストケースを分割

Slide 17

Slide 17 text

© 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 { ... 正常って何? 何がどうなっていれば正しい? テスト対象の関数名が テストケース名に含まれている

Slide 18

Slide 18 text

© 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 { ...

Slide 19

Slide 19 text

© 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パターンのコメントを追加して テストの流れを整理する

Slide 20

Slide 20 text

© 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 に分割して整理する

Slide 21

Slide 21 text

© 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 { 他のテストケースでも同じ処理を使って いるので関数化している

Slide 22

Slide 22 text

© 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) } 他のテストケースと同じ処理であっても 関数の切り出しをしない

Slide 23

Slide 23 text

© 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歳の誕生日とな る日付を計算している

Slide 24

Slide 24 text

© 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歳の誕生日と なる日をベタ書きする

Slide 25

Slide 25 text

© asken.inc 25 4. 実際にどんな効果があったか

Slide 26

Slide 26 text

© asken.inc 26 askenエンジニアが実感している効果 期待される効果を実際に体験できている ● バグの早期発見 ● リグレッションエラー(デグレ)の検出 ● 設計改善

Slide 27

Slide 27 text

© asken.inc 27 予想外の効果 iOSエンジニアにテストコードの レビューをしてもらえるようになった これまでの対応によりKotlinに不慣れでも テストコードの内容は理解できる レビューしてもらった結果… iOS - Android間の仕様差異を事前に検出できた!!

Slide 28

Slide 28 text

© asken.inc 28 We are hiring! askenで一緒に活躍してくれる方を募集しています!

Slide 29

Slide 29 text

© asken.inc 29 Thank you!