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
Androidテストハンズオン: テストのないアプリにテストを書こう編 / handson-w...
Search
tkmnzm
April 11, 2019
Programming
1
4.3k
Androidテストハンズオン: テストのないアプリにテストを書こう編 / handson-write-tests-for-app-without-test
tkmnzm
April 11, 2019
Tweet
Share
More Decks by tkmnzm
See All by tkmnzm
AndroidアプリのUIバリエーションをあの手この手で確認する / Check UI variations of Android apps by various means
tkmnzm
1
950
Androidアプリの良いユニットテストを考える / Thinking about good unit tests for Android apps
tkmnzm
5
7.8k
Google I:O 2023 Androidの自動テストアップデートまとめ / Google I:O 2023 Android Testing Update Recap
tkmnzm
0
580
コルーチンのエラーをテストするためのTips / Tips for testing Kotlin Coroutine errors
tkmnzm
0
950
Androidのモダンな技術選択にあわせて自動テストも アップデートしよう / Update your automated tests to match Android's modern technology choices
tkmnzm
3
2.1k
SWET dev-vitalチームによるプロジェクトの健康状態可視化の取り組み / SWET dev-vital team's efforts to visualize the health of the project
tkmnzm
1
1.2k
モバイルアプリテスト入門 / Getting Started with Mobile App Testing
tkmnzm
1
500
25分で作るAndroid Lint / Android Lint made in 25 minutes
tkmnzm
0
860
2年半ぶりのプロダクト開発であらためて感じた自動テストの大切さ / realized the importance of automatic testing with product development for the first time in two and a half years
tkmnzm
1
770
Other Decks in Programming
See All in Programming
情報漏洩させないための設計
kubotak
0
120
PHPで学ぶプログラミングの教訓 / Lessons in Programming Learned through PHP
nrslib
2
220
良いユニットテストを書こう
mototakatsu
7
2.2k
Stackless и stackful? Корутины и асинхронность в Go
lamodatech
0
740
短期間での新規プロダクト開発における「コスパの良い」Goのテスト戦略」 / kamakura.go
n3xem
2
170
たのしいparse.y
ydah
3
120
rails stats で紐解く ANDPAD のイマを支える技術たち
andpad
1
290
これが俺の”自分戦略” プロセスを楽しんでいこう! - Developers CAREER Boost 2024
niftycorp
PRO
0
190
生成AIでGitHubソースコード取得して仕様書を作成
shukob
0
350
Zoneless Testing
rainerhahnekamp
0
120
Amazon S3 NYJavaSIG 2024-12-12
sullis
0
100
テスト自動化失敗から再挑戦しチームにオーナーシップを委譲した話/STAC2024 macho
ma_cho29
1
1.3k
Featured
See All Featured
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
6
520
A better future with KSS
kneath
238
17k
A Tale of Four Properties
chriscoyier
157
23k
Agile that works and the tools we love
rasmusluckow
328
21k
Art, The Web, and Tiny UX
lynnandtonic
298
20k
StorybookのUI Testing Handbookを読んだ
zakiyama
27
5.3k
Navigating Team Friction
lara
183
15k
Rails Girls Zürich Keynote
gr2m
94
13k
GraphQLの誤解/rethinking-graphql
sonatard
67
10k
Making Projects Easy
brettharned
116
5.9k
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
169
50k
Music & Morning Musume
bryan
46
6.2k
Transcript
Androidテストハンズオン テストのないアプリにテストを書 こう編
今日の目標 • テストが書きにくいアプリを改善する体験をしてもらい 実プロジェクトでトライする取っ掛かりにする 2
アジェンダ • レガシーコードを改善しよう • レガシーコード改善に役立つ知識 − テストダブル − レガシーコード改善テクニック •
テストのないアプリにテストを書こう 3
レガシーコードを改善しよう 4
社内テスト実施状況アンケートの結果 • ユニットテストを「すでに導入しているが、もっと力をいれていき たい」「導入したいが、できていない」を合計して約75% • ユニットテストにおける課題として、50%が「テストが書きにくい 設計になっている」と回答 5
テストが書きにくい設計になっている • 新規に実装する − TDD等の手法を駆使しつつ、テストが書きやすい設計にする • 既存のコードはどうする? − ごっそり書き直す VS
地道に改善を重ねる − ごっそり書き直すチャンスを得られるのは稀。そして成功する保証はない。 6
地道に改善を重ねる • レガシーコード改善のすすめ − レガシーコードとは、単にテストのないコード(レガシーコード改善ガイドより) − テストを実装しながらテストが書きにくい設計を改善していく • 改善をしないとどうなる? −
レガシーコードに変更を加えていくことで、自分自身もレガシーコードを積み上 げてしまう悲劇 7
レガシーコード改善のアプローチ • 保護して変更する − 変更する箇所をテストで保護をした上で安全にリファクタリング − ユニットテストは開発中に素早くフィードバックを得られる − このアプローチが辛い場合、手動での動作確認も代替案として有効 8
レガシーコード改善手順 1. 変更点を洗い出す 2. テストを書く場所を見つける 3. 依存関係を排除する 4. テストを書く 5.
変更とリファクタリングを行う 9
レガシーコード改善に役立つ知識 10
引用・参考について前置き • この章の内容・ソースコードは下記本の内容を引用・参考にし て作成しています。 − 『Androidテスト全書』 − 著者: 白山 文彦,
外山 純生, 平田 敏之, 菊池 紘, 堀江 亮介 − https://peaks.cc/books/android_testing − 『レガシーコード改善ガイド』 − 著者: マイケル・C.フェザーズ 11
テストダブル • テスト対象が依存しているコンポーネントを本物そっくりに振る 舞う代役(ダブル)と差し替えることで、自分の期待する挙動や 値の返却を達成する • レガシーコード改善の中では、テスト対象が依存しているクラ ス・コントロールしたいクラスをテストダブルに置き換えながら テストを整備していく 12
テストダブル • 5つのパターン − スタブ − モック − スパイ −
フェイク − ダミー 13
スタブ • 事前に定義した任意の値をテスト対象に与える • 依存コンポーネントをスタブに置き換えることで、テスト対象に 任意の値を渡すことができる 14
スタブ実装例(テスト対象コード) public class WeatherForecast { private Sattelite satellite; boolean shouldBringUmbrella()
{ Weather weather = satellite.getWeather(); switch (weather) { case RAINY: return true; default: return false; } } } 15 satellite.getWeatherがRAINYのときのみ trueを返すメソッドをテストする
Libraryを用いたスタブの作成 Mockito/mockKともにモック・スパイオブジェクトがstubの機能を備えている Mockito(Java) Sattelite mock = mock(Sattelite.class); when(mock.getWeather()).thenReturn(Weather.RAINY); WeatherForecast testTarget
= new WeatherForecast(mock); boolean result = testTarget.shouldBringUmbrella(); assertThat(result).isEqualTo(true); 16 任意の値を返却する設定
スタブの機能を実装する open class Satellite { open fun getWeather(): Weather {
/* 元々の実装 */ } } /* 任意のWeatherを返すことができるスタブ */ class StubSatellite(val anyWeather: Weather) : Satellite() { override fun getWeather(): Weather { return anyWeather } } 17 任意の値を渡す 設定した任意の値を 返却
モック 18 • テスト対象が依存コンポーネントに与える値や挙動(出力)を検 証する
モック実装例(テスト対象コード) public class WeatherForecast { private WeatherRecorder recorder; void recordCurrentWeather(Weather
weather) { recorder.record(weather); } } 19 内部でWeatherRecorder#recordを呼び出し ているメソッドをテストする
Mockito(Java) WeatherRecorder mock = mock(WeatherRecorder.class); WeatherForecast testTarget = new WeatherForecast(mock);
testTarget.recordCurrentWeather(Weather.SUNNY); verify(mock).record(Weather.SUNNY); Libraryを用いたモックの作成 20 メソッドが呼び出されたかと 与えられた値の検証
モックの機能を実装する 21 open class WeatherRecorder { open fun record(weather: Weather)
{} } class MockWeatherRecorder : WeatherRecorder() { var weather: Weather? = null var isCalled = false override fun record(weather: Weather) { this.weather = weather isCalled = true } } メソッドの呼び出しと 与えられた値を記録
スパイ 22 • テスト対象が依存コンポーネントに与える値や挙動(出力)を記 録する • モックライブラリで提供されているスパイは、 実際にメソッドコールを行う振る舞いをすることが多い
スパイ実装例(テスト対象コード) public class WeatherForecast { private WeatherRecorder recorder; void recordCurrentWeather(Weather
weather) { recorder.record(weather); } } 23 モックの際と同様のメソッドをテスト
Libraryを用いたスパイの作成 Mockito(Java) WeatherRecorder spy = spy(new WeatherRecorder()); WeatherForecast testTarget =
new WeatherForecast(mock); testTarget.recordCurrentWeather(Weather.SUNNY); verify(mock).record(Weather.SUNNY); 24 クラスのインスタンスを 渡して生成する
スパイの機能を実装する 25 open class WeatherRecorder { open fun record(weather: Weather)
{} } class SpyWeatherRecorder : WeatherRecorder() { var weather: Weather? = null var isCalled = false override fun record(weather: Weather) { this.weather = weather isCalled = true super.record(weather) } } 実クラスのメソッドを 呼び出し
[補足] xUnit Test Patternsでの定義 • 現在のモックライブラリで提供されている機能とxUnit Test Patternsでの定義とは全く一緒ではない場合がある − テストダブルはx
Unit Test Patternsで定義された用語 − 参考リンク − https://www.amazon.co.jp/dp/B004X1D36K/ − http://xunitpatterns.com/Test%20Double.html − http://goyoki.hatenablog.com/entry/20120301/1330608789 26
フェイク・ダミー 27 • フェイク − 実際のコンポーネントと同等かそれに極めて近い挙動を持つ実装オブジェクト − 例: RoomのInMemoryDatabase •
ダミー − テストの結果に影響を与えないが、テスト対象クラスの生成・メソッドの呼び出 しに使用する代替オブジェクト
テストダブルの使いどころ 28 テストしたいクラス 依存しているクラス APIとの通信で実行時間がかかる など、いやな副作用がある 特定の振る舞いをテストしたいが、 再現するのが大変 依存しているクラスのメソッドが正し い引数で呼ばれているかわからな
い 参照
テストダブルの使いどころ 29 テストしたいクラス 依存しているクラス このクラスをテストダブルに 置き換えてテストする
テストダブルの使いどころ 30 テストしたいクラス 依存している クラスのテストダブル 特定の振る舞いを再現する値を返 すようにする メソッド呼び出し時の引数の値を記 録して検証できるようにする APIとの通信など副作用がある処
理を実行しない
レガシーコード改善テクニック 31
レガシーコードのジレンマ 32 • コードを変更するためには、テストを整備する必要がある。多く の場合、テストを整備するためには、コードを変更する必要が ある。 − レガシーコード改善ガイドでは、最低限のリファクタリングでテストを書けるよう にするテクニックを紹介している
テストの書きにくいコードが抱える課題 33 • テストしたいコードがAPI通信などテストコードで 実行したくない機能に依存している • テストしたいコードがグローバルなSingletonクラスに 依存している • テストしたいコードがprivateになっている
• ゴッドクラス/モンスターメソッド • Fat Activity/Fragment
コンストラクタのパラメータ化 34 • クラス内部でインスタンスの生成をしている際に、コンストラク タのパタメータとして外から渡すようにする • ユニットテストを書く際に不都合なDB・APIへのアクセスなどを テストダブルに差し替えることができる • テストしたいコードから依存を切り離すための基本的なアプ
ローチ
コンストラクタのパラメータ化 35 class ToDoItemRepository() { val api = ToDoItemApi() fun
save(toDoItem :ToDoItem) { api.save(toDoItem) } } @Test fun test() { val repository = ToDoItemRepository() save(ToDoItem("walking")) } APIへの送信が行われてしまう
コンストラクタのパラメータ化 36 class ToDoItemRepository(val api: ToDoItemApi) { fun save(toDoItem: ToDoItem)
{ api.save(toDoItem) } } @Test fun test() { val mockApi = mock(ToDoItemApi::class.java) val repository = ToDoItemRepository(mockApi) save(ToDoItem("walking")) verify(mockApi).save(ToDoItem("walking")) } モックオブジェクトをコンストラクタに渡すこ とでAPIの通信が行われない かつ与えた値をverifyで検証可能になる
静的setメソッドの導入 37 • Singletoneクラスにインスタンスを差し替えるsetterを作成し、 テストダブルを差し込めるようにする • テストしたいクラスがSingletonクラスを参照していて、テストを 書くのが難しい場合に有効 • setterには@VisibleForTestingアノテーションをつけてあげる
静的setメソッドの導入 38 class ToDoItemRepository(val api: ToDoItemApi) { fun save(toDoItem: ToDoItem)
{ // singletonなクラスへのアクセス Analytics.log("save todo item") api.save(toDoItem) } } @Test fun test() { val repository = ToDoItemRepository(mockApi) save(ToDoItem("walking")) verify(mockApi).save(ToDoItem("walking")) } Analyticsへの送信が行われてしまう
静的setメソッドの導入 39 class Analytics private constructor() { companion object {
private var instance: Analytics? = null fun getInstance(): Analytics { return instance } @VisibleForTesting fun setInstance(analytics: Analytics) { instance = analytics } } fun log(event: String) { } } Analyticsインスタンス差し替え setterを作成
静的setメソッドの導入 40 @Test fun test() { val mockAnalytics = mock(Analytics::class.java)
Analytics.setInstance(mockAnalytics) val repository = ToDoItemRepository(mockApi) save(ToDoItem("walking")) verify(mockApi).save(ToDoItem("walking")) } モックオブジェクトをインスタンスに セットすることでAnalyticsへの 送信が行われないようにする
@VisibleForTesting • 可視性をテスタビリティのために広く定義していることを示すア ノテーション • アノテーションがつけられたクラス・メソッド・フィールドをプロダ クトコードから呼び出すと、InspectionでWarningがでる 41
privateメソッドのテスト 42 • privateメソッドを新しいクラスとして抽出できないか? − クラスが多くのことを行いすぎていることが多い • publicメソッドを通じてテストが可能か? • 上記2つが難しい場合、メソッドに@VisibleForTestingアノテー
ションをそえた上で、package privateやinternalにする Kotlinのアクセス修飾子 同じモジュール内からアクセス可の意
テストのためにsetter作成や可視性をさげること 43 • 理想的な設計ではない − パラメータで渡せるようにしたり、クラスとして抽出するべき − 理想的な設計にするためには、大規模な改修になってしまうこともある • テストを整備しておけば、あとでコードをもっときれいすること
が可能 • @VisibleForTestingを修正時の目印にする
スプラウトメソッド・スプラウトクラス 44 • 要件を追加する際に新しいメソッド or クラスとして実装し、既 存のコードには新しいメソッド or クラスの呼び出しのみ追加す る
• 新しく追加したコードにはテストを作成する • 既存のコードにテストを書くのは諦める • ゴッドクラス・モンスターメソッド・FatActivty(Fragment)との戦 闘を一旦回避するために有効
スプラウトメソッド・スプラウトクラス 45 スプラウトクラス 依存が多くて インスタンスを 作るのが大変... すでにクラスが 大きくて 変更したくない...
リファクタリング 46 • リファクタリングはどのように進めていく? • 『リファクタリング』本の内容は現役で参考になる • 本の内容をチャートでまとめてくれているサイトもあるので、リ ファクタリング時に対象のクラスにどの方針をとればいいか確 認してみる
− http://objectclub.jp/technicaldoc/refactoring/refact-smell
IntelliJのリファクタリング機能 47 • レガシーコードの改善&リファクタリング時に、IntelliJのリファク タリング機能を使うことでより安全に作業ができる − https://pleiades.io/help/idea/refactoring-source-code.html • ショートカット例 −
⌃T : 使用できるリファクタリング機能の一覧表示 − option + ⌘ + M : メソッドとして切り出す(Extract Method) − option + ⌘ + P : パラメータとして切り出す(Extract Parameter) − クラスを選択するとコンストラクタのパラメータとして切り出される