フロントエンドのテスト戦略を考える(Flutter編)
Flutterには皆さん御存知の通り、以下の3つのテストが用意されています。
- Unit Test
- Widget Test
- Integration Test
Unitテストは関数単位のテスト、Widget TestはComponent単位のテスト、Integration TestはE2Eテストです。
今の方針
- Unit TestはJSONのTypeSafe確認テストぐらい。
- Widget Testはスクショ比較で状態管理の確認をやりたいところだけやる。
- Integration Testは正常シナリオ1本書くだけ。
Unit TestはJSONのパースができるかどうか、ぐらい
バックエンドのように、あるオブジェクトを渡して別のオブジェクトを作るデータ変換と、メソッドチェーンのような形でワークフローを回す的なコードを書くことが、Flutterで書くことがほとんどない。データの整形はあっても、JSON色付けが9割。
困るのは、freezed
とかで作ったモデルにデシリアライズする時にTypeErrorやNullでコケることなんで、指定されたJSONをfromJson
して読み取って、ちゃんと狙ったオブジェクトが作れるかどうか。これはテストを書く意味がある。
Flutter関係ないけど、API仕様書で一番気になるのはそのデータがOptionalかどうか。つまり、hoge: null
であったり、hoge
というキーがそもそも無いというケースが存在するのかどうか。Optionalなのにrequiredで指定したらNullで落ちるので。
if/forみたいなロジックの9割はWidgetで書くから、Unit Testの対象になり得るのはJSONのパース・数値演算・文字列のフォーマットぐらいしか思いつかぬ。それに、フォーマットや数値演算の結果は絶対どこかのWidgetで表示するから、Widget Testでチェックすればいいでしょう。
Widget Testで担保できるものは少ない
Widget Testで確認できるのは、デザイン通りになっているかと、状態に応じたボタンや表示の切り替えぐらいのはず。外部接続できないので。特に画像が辛い。いくらAPIをモックしたところで、モックした画像がhttpから始まってたら読めない。Assetもだめかも。そしたらBASE64にしてImage.memoryするしかないなー。皆さんどうしてるんだこれ。
Widgetに仕込んだロジックが意図した通りに動いて、その見た目も狙ったデザイン通りだよね?を確認するためのテストだと思っている。
在庫がない商品の詳細を開いたら、「売り切れ」になっていて、カートに追加するボタンが出てこない、とか。サイズを選択したら、サムネやカラーバリエーションの一覧が変更される、とか。残り在庫数が3個以下の場合、「残りN点」って表示される、など。
こういうのはスクショ比較で行えばよいと思うので、こーゆーデータが来たら、このスクショのとおりになること。ここでMサイズをタップしたら、このスクショのとおりになること・・・みたいな書き方で良い気がします。
もう1つ重要なのが、例外処理。APIからこんな例外が返ってきた時に、ちゃんと意図した見た目になっているか、適切なメッセージが表示されているか、などはテストが必要だと思います。それをやるには、例外をそのまま投げるんじゃなくて型として返す(Success/Failure的な)設計にしないといけないし、そうなってないならまずはそこからやろう。
表示の出し分けをしないコンポーネントは、テストの記述は任意で良いと思います。
Integration Testは正常系シナリオ1本が理想
Integration Testで、こういう例外が発生したら・・・みたいなテストは要らない。こういう時にこの見た目になる・・・も同様。そこはバックエンド関係ないから。
何を書くかといえば、このシナリオが死んだら絶対に困るというものをテストする。ECでいえば、ログイン→商品閲覧→カートIN→注文送信→履歴確認だと思う。モノが買えなかったら終わる。外部接続(API連携や、SecureStorage・ローカルDB等)がちゃんと動いているかも、正常系シナリオ1本通すことでブラックボックス的に確認できる。
重要なのは、データを送る処理が正常に受け付けられるか。ECだと、お気に入りにいれる、クーポンを適用する、カートに追加する、カートの数量をいじる、注文を飛ばす・問い合わせをする・自分の情報を書き変える・・・等。ユーザーが飛ばしたデータがきちんと受け付けられているか、ちゃんと選択した内容通りのデータが飛んでるかどうか、などをIntegration Testの中でチェックする。
なので、Integration Testで書くことは、1つの正常系(前述した、ログイン→商品閲覧→カートIN→注文送信→履歴確認)シナリオだけで充分だと思う。会員登録も必要かな。
困るのがWebView。WebViewを出すだけならいいけど、WebViewにある該当のボタンをタップするという操作が辛い。WebViewの内部はIntegration Testで触ることが出来ないので。
JavaScript連携はできるから、Integration Test Buildだけ見えないボタンを作って、そいつをTapして、WebViewのJSにDOM操作仕込んで動かす的な対応になるのかな。WebView嫌い。
デバイスプレビューどうしたらいいんやろ
色んなデバイスで見た目をプレビューしたいという要件は絶対あって、device_preview
などのライブラリはあるけど、Webでビルドすることになるので、アプリに依拠したライブラリとか入っていてもいけるんですかね。。。SecureStorage/SharedPrefernce/sqfliteとか。
また、ダメだった場合RiverpodなどでMockする必要がありそうなんですけど、DIを注入してもちゃんとプレビューできるのかな。できてほしいのだか。
閑話休題:Mockableにする所
Riverpodでoverrideすればモックしやすくなる所ですが、Widgetに触るレイヤーだけでいいでしょう。例えばレポジトリクラスをグローバルに引き回すためにRiverpod使うとか、あまり意味がないと思う。Widgetに公開するのはRiverpodのProviderだけ。そのProviderが内部でAPI叩こうが何しようが、Widgetは知る必要がないから。
FutureProviderの中身を、Future.value({"foo": "bar"})
にできればそれでいい。Notiferになるとクラスをまるっと差し替える必要があるので、MockTailやtestなどを使ってFakeなオブジェクトを作って渡すことになりそう。MockTailじゃなくてもいいんじゃないかな。パラメーターの組み合わせで戻り値の返り方が違うロジックが、フロントで組み込まれている時点で微妙。
Omiaiさんで、Widgetが直レポジトリ(new HogeRepository())を噛ましたらPR以前でLintエラーにしたい的な話が上がってたけど、これを機械的にやれる方法があると良いね。Devinでチェック入れることもできるのかな?
自動テスト(なるべく)書きたくない
テストの本数と品質担保ができるかどうかは、関係がない。テストコードも負債になりえる。理想は90点を取るために最小のテスト数で担保できることだと思います。おわり。
Discussion