🚶‍♂️

フロントエンドのテスト戦略を考える(Flutter編)

に公開

Flutterには皆さん御存知の通り、以下の3つのテストが用意されています。

  1. Unit Test
  2. Widget Test
  3. 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