Slide 1

Slide 1 text

Androidアプリの テスト駆動開発入門 トニオ

Slide 2

Slide 2 text

自己紹介 • トニオ(@tonionagauzzi) • Android Engineer @cybozu 2

Slide 3

Slide 3 text

発表内容 • 以下のブログの内容です! https://blog.cybozu.io/entry/2024/05/24/170000 3

Slide 4

Slide 4 text

Androidでテスト駆動開発(TDD) 始めました! 4

Slide 5

Slide 5 text

TDDとは • 動作するきれいなコードを作るために、テストによって開発を推し進 める 5

Slide 6

Slide 6 text

TDD導入の背景 • レガシーコードいっぱい… テスト作りたい! 書き換えたい! テストがなくて不安… テスト作れる構造じゃない… 6

Slide 7

Slide 7 text

新しいモジュールぐらいは テスト書こう! 7

Slide 8

Slide 8 text

TDD導入の背景 • TDDで作ると oテストしやすくなるらしい! o コードも綺麗になるらしい! o 依存モジュール(レガシーコード)も少しずつテストできそう! 8 Testable Module Legacy Module テスト対象

Slide 9

Slide 9 text

今回は、TDDのテストは自動テスト という前提で話します! 9

Slide 10

Slide 10 text

TDDの流れ 10

Slide 11

Slide 11 text

TDDの実装例! 11

Slide 12

Slide 12 text

ログイン画面にヘルプリンクを追加 • ユーザーストーリー o ログイン操作を行うユーザーとして、ログイン操作につまずいたとき、 その画面上から直接関連するヘルプページを開きたい。 o なぜなら、疑問の答えを一から探す手間をかけずに 関連するヘルプページを参照し、ログインを完了させ、 本来やりたい業務を進められるからだ。 o • 受け入れ条件 o ログイン画面からヘルプページを開けること o 端末の言語設定に応じてヘルプの言語を変えること o 接続先ごとに提供サービスが異なるため、接続先に応じてヘルプの内容を変えること 12

Slide 13

Slide 13 text

(1) 接続先情報と端末の言語設定に応じたヘルプページのURLを生成する 日本語 / 日本サーバー の組合せ 英語 / 日本サーバー の組合せ 簡体字 / 日本サーバー の組合せ 繁体字 / 日本サーバー の組合せ その他 / 日本サーバー の組合せ 日本語 / 米国サーバー の組合せ 英語 / 米国サーバー の組合せ 簡体字 / 米国サーバー の組合せ 繁体字 / 米国サーバー の組合せ その他 / 米国サーバー の組合せ 接続先を判定できなかった場合、日本語 / 日本サーバー の組合せとなる (2) ログイン画面でヘルプリンクが押されると、1.で生成したURLを Chromeに渡し、ヘルプページを開く CHECK LIST 13

Slide 14

Slide 14 text

// テスト @Test fun `日本語 / 日本サーバー の組合せで、ヘルプページのURLを生成する` { // Given(前提条件) val language = "ja" val country = "JP" // When(操作) val url = get(language = language, country = country) // Then(結果) Truth.assertThat(url).isEqualTo("https://localhost/help/ja/JP/") } // 実装 fun get(language: String, country: String): String { return "" } (1) 接続先情報と端末の言語設定に応じたヘルプページのURLを生成する 日本語 / 日本サーバー の組合せ RED 14

Slide 15

Slide 15 text

// テスト @Test fun `日本語 / 日本サーバー の組合せで、ヘルプページのURLを生成する` { // Given(前提条件) val language = "ja" val country = "JP" // When(操作) val url = get(language = language, country = country) // Then(結果) Truth.assertThat(url).isEqualTo("https://localhost/help/ja/JP/") } // 実装 fun get(language: String, country: String): String { return "https://localhost/help/ja/JP/" } (1) 接続先情報と端末の言語設定に応じたヘルプページのURLを生成する 日本語 / 日本サーバー の組合せ GREEN 15

Slide 16

Slide 16 text

// テスト @Test fun `日本語 / 日本サーバー の組合せで、ヘルプページのURLを生成する` { // Given(前提条件) val language = "ja" val country = "JP" // When(操作) val url = get(language = language, country = country) // Then(結果) Truth.assertThat(url).isEqualTo("https://localhost/help/ja/JP/") } @Test fun `英語 / 日本サーバー の組合せで、ヘルプページのURLを生成する` { // Given val language = "en" val country = "JP" // When val url = get(language = language, country = country) // Then Truth.assertThat(url).isEqualTo("https://localhost/help/en/JP/") } // 実装 fun get(language: String, country: String): String { return "https://localhost/help/ja/JP/" } (1) 接続先情報と端末の言語設定に応じたヘルプページのURLを生成する 日本語 / 日本サーバー の組合せ 英語 / 日本サーバー の組合せ RED 16

Slide 17

Slide 17 text

// テスト @Test fun `日本語 / 日本サーバー の組合せで、ヘルプページのURLを生成する` { // Given(前提条件) val language = "ja" val country = "JP" // When(操作) val url = get(language = language, country = country) // Then(結果) Truth.assertThat(url).isEqualTo("https://localhost/help/ja/JP/") } @Test fun `英語 / 日本サーバー の組合せで、ヘルプページのURLを生成する` { // Given val language = "en" val country = "JP" // When val url = get(language = language, country = country) // Then Truth.assertThat(url).isEqualTo("https://localhost/help/en/JP/") } // 実装 fun get(language: String, country: String): String { if (language == "en") { return "https://localhost/help/en/JP/" } else { return "https://localhost/help/ja/JP/" } } (1) 接続先情報と端末の言語設定に応じたヘルプページのURLを生成する 日本語 / 日本サーバー の組合せ 英語 / 日本サーバー の組合せ GREEN 17

Slide 18

Slide 18 text

// テスト @Test fun `日本語 / 日本サーバー の組合せで、ヘルプページのURLを生成する` { // Given(前提条件) val language = "ja" val country = "JP" // When(操作) val url = generateHelpLink(language = language, country = country) // Then(結果) Truth.assertThat(url).isEqualTo("https://localhost/help/ja/JP/") } @Test fun `英語 / 日本サーバー の組合せで、ヘルプページのURLを生成する` { // Given val language = "en" val country = "JP" // When val url = generateHelpLink(language = language, country = country) // Then Truth.assertThat(url).isEqualTo("https://localhost/help/en/JP/") } // 実装 fun generateHelpLink(language: String, country: String): String { val helpPath = when (language) { "en" -> "en/JP" else -> "ja/JP" } return "https://localhost/help/$helpPath/" } (1) 接続先情報と端末の言語設定に応じたヘルプページのURLを生成する 日本語 / 日本サーバー の組合せ 英語 / 日本サーバー の組合せ REFACTOR 18

Slide 19

Slide 19 text

1つずつテスト、実装、リファクタリング! RED GREEN REFACTOR 19

Slide 20

Slide 20 text

(1) 接続先情報と端末の言語設定に応じたヘルプページのURLを生成する 日本語 / 日本サーバー の組合せ 英語 / 日本サーバー の組合せ 簡体字 / 日本サーバー の組合せ 繁体字 / 日本サーバー の組合せ その他 / 日本サーバー の組合せ 日本語 / 米国サーバー の組合せ 英語 / 米国サーバー の組合せ 簡体字 / 米国サーバー の組合せ 繁体字 / 米国サーバー の組合せ その他 / 米国サーバー の組合せ (2) ログイン画面でヘルプリンクが押されると、1.で生成したURLを Chromeに渡し、ヘルプページを開く 20

Slide 21

Slide 21 text

(1) 接続先情報と端末の言語設定に応じたヘルプページのURLを生成する 日本語 / 日本サーバー の組合せ 英語 / 日本サーバー の組合せ 簡体字 / 日本サーバー の組合せ 繁体字 / 日本サーバー の組合せ その他 / 日本サーバー の組合せ 日本語 / 米国サーバー の組合せ 英語 / 米国サーバー の組合せ 簡体字 / 米国サーバー の組合せ 繁体字 / 米国サーバー の組合せ その他 / 米国サーバー の組合せ (2) ログイン画面でヘルプリンクが押されると、1.で生成したURLを Chromeに渡し、ヘルプページを開く 21

Slide 22

Slide 22 text

Androidの振る舞いをTDDしよう! 22

Slide 23

Slide 23 text

private val activity = Robolectric.buildActivity(FragmentActivity::class.java).create().start().resume().get() // テスト @Test fun `ログイン画面でヘルプリンクが押されると、生成したURLをChromeに渡し、ヘルプページを開く`() { // Given (前提条件: ログイン画面を開いている) val fragment = Fragment() activity.add(fragment) val helpLinkUrl = generateHelpLink(language = "ja", country = "JP") // When (操作:ヘルプリンクを押す) fragment.openBrowser(url = helpLinkUrl) // Then (結果:ChromeがURLを開く) val openedActivity = Shadows.shadowOf(activity).nextStartedActivity val openedPackage = openedActivity.getPackage() Truth.assertThat(openedPackage).isEqualTo("com.android.chrome") val openedUrlString = openedActivity.data.toString() Truth.assertThat(openedUrlString).isEqualTo(helpLinkUrl) } // 実装 fun Fragment.openBrowser(url: String) { } (1) 接続先情報と端末の言語設定に応じたヘルプページのURLを生成する 日本語 / 日本サーバー の組合せ 英語 / 日本サーバー の組合せ 簡 簡 簡 / 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 繁体字 / 日本サーバー の組合せ その他 / 日本サーバー の組合せ 日本語 / 米国サーバー の組合せ 英語 / 米国サーバー の組合せ 簡体字 / 米国サーバー の組合せ 繁体字 / 米国サーバー の組合せ その他 / 米国サーバー の組合せ 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 / 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 (2) ログイン画面でヘルプリンクが押されると、1.で生成したURLを Chromeに渡し、ヘルプページを開く 23

Slide 24

Slide 24 text

private val activity = Robolectric.buildActivity(FragmentActivity::class.java).create().start().resume().get() // テスト @Test fun `ログイン画面でヘルプリンクが押されると、生成したURLをChromeに渡し、ヘルプページを開く`() { // Given (前提条件: ログイン画面を開いている) val fragment = Fragment() activity.add(fragment) val helpLinkUrl = generateHelpLink(language = "ja", country = "JP") // When (操作:ヘルプリンクを押す) fragment.openBrowser(url = helpLinkUrl, errorCallback = { e -> fail(e) }) // Then (結果:ChromeがURLを開く) val openedActivity = Shadows.shadowOf(activity).nextStartedActivity val openedPackage = openedActivity.getPackage() Truth.assertThat(openedPackage).isEqualTo("com.android.chrome") val openedUrlString = openedActivity.data.toString() Truth.assertThat(openedUrlString).isEqualTo(helpLinkUrl) } // 実装 fun Fragment.openBrowser( url: String, errorCallback: (e: Exception) -> Unit = { e -> // エラー処理 }, ) { try { val chromeIntent = Intent(Intent.ACTION_VIEW, url.toUri()) chromeIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK chromeIntent.setPackage("com.android.chrome") startActivity(chromeIntent) } catch (e: Exception) { errorCallback(e) } } (1) 接続先情報と端末の言語設定に応じたヘルプページのURLを生成する 日本語 / 日本サーバー の組合せ 英語 / 日本サーバー の組合せ 簡 簡 簡 / 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 繁体字 / 日本サーバー の組合せ その他 / 日本サーバー の組合せ 日本語 / 米国サーバー の組合せ 英語 / 米国サーバー の組合せ 簡体字 / 米国サーバー の組合せ 繁体字 / 米国サーバー の組合せ その他 / 米国サーバー の組合せ 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 / 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 簡 (2) ログイン画面でヘルプリンクが押されると、1.で生成したURLを Chromeに渡し、ヘルプページを開く 24

Slide 25

Slide 25 text

• Androidはテスト支援ツールが充実! oユニットテストならRobolectric oandroidTestならEspresso、UI Automator https://github.com/robolectric/robolectric https://developer.android.com/training/testing/espresso https://developer.android.com/training/testing/other-components/ui-automator 25

Slide 26

Slide 26 text

• 可能な限りユニットテストで書く! oテストピラミッドの考え方 o忠実性は下がる 図:https://zenn.dev/jyoppomu/articles/52844385940140 26

Slide 27

Slide 27 text

TDDで感じたメリット • 実装に取り掛かりやすい! ➢大きな機能を小さく分けて作れるし、 が気持ちいい! • 実装に自信を持ちやすい! ➢チェックリストとテストがいつでも正しさを証明してくれる! 27

Slide 28

Slide 28 text

TDDで感じたメリット • TDDで作った実装は壊れにくい! ➢テストがあるから安心して修正できる! • テストの整備が進む! ➢開発プロセスがどんどん良くなる! 28

Slide 29

Slide 29 text

TDDが難しいとき • テストがなくても自信を持てる実装 ➢ログを1行だけ足すとか • 画面の見た目が変わるような実装 ➢画面回帰テスト(VRT)が向いてる 29

Slide 30

Slide 30 text

TDDが難しいとき • テストコードを過信してしまう ➢テストの不足や誤りがないかは常に疑うべし • チームの認識が揃っていない ➢議論が長引くとTDDのスピードが上がらない ➢テスト、実装、リファクタリングの1サイクルは数分ほどで行われる ➢メンバーの知識や方向性を揃えるほうが先かも 30

Slide 31

Slide 31 text

テスト自動化の優先度 31

Slide 32

Slide 32 text

こんなテストも自動化しました! • PUSHを受信したら、通知バーに通知が表示されること o テストコードでgoogle-auth-library-oauth2-httpを使ってFCMにPUSH o 通知の検証はNotificationManagerを使う 32

Slide 33

Slide 33 text

こんなテストも自動化しました! • WebViewからJavascriptが呼ばれたら、ホーム画面にアプリの ショートカットが追加されること o テストコードでlaunchFragmentInContainerでWebViewをシミュレート o WebView.evaluateJavascriptでJSを発火 oショートカットの検証はShortcutManagerを使う 33

Slide 34

Slide 34 text

まとめ • チェックリストを作り、テスト、実装、リファクタリング • TDDで実装に自信を持つのが一番大事! ▪ テストから書くルールやテストが増えることは副次的効果に過ぎない 34

Slide 35

Slide 35 text

Thank you for watching! 35