ライブラリのテストは、ほとんどの場合 unit testでいいと思うのだけど native_webviewはiOSのWKWebViewやAndroid の WebViewに依存しているのでunit testではテストが出来ない。
なので、native_webview は integration test (ほとんど Widget testだけど)を書いていて、それを手元やCirrusCIで動かしている。その時に得た知見を書いていく。
ちなみにintegration test は実行時間が10〜20分くらいでかなり長くなるのだが、CirrusCIは無料でも1時間実行出来たりMacが使えたりとOSSではコスパが異常に良いので使っている。
どのように動かしているか
.cirrus.yml を見るのが早い。
https://github.com/hisaichi5518/native_webview/blob/master/.cirrus.yml
iOS, Androidともに実機でテストはしていなくて、SimulatorやEmulatorを立ち上げてflutter driveでテストを実行している。SimulatorやEmulatorを立ち上げるために行う処理が多いけど、flutter drive自体は簡単に実行できる。
Android と iOS で動作が違う
しょうがないと言えばしょうがないのだけど、Android と iOS のWebViewで動作が違うことがある。具体的には、loadUrlを読んだ時にAndroidはShouldOverrideUrlLoadingが呼ばれなかったりするがiOSは呼ばれる。
しょうがないのでテストでは Platfrom.isAndroid とかで分岐したりスキップしたりして対応している。
ちなみにメンテナンスしにくいなと思って一回ファイルを分けたりしていたのだけど、AndroidではテストしてiOSではテストしてなかったみたいなことがあったので、結局まとめて分岐で対応することにした。
実行するたび結果が変わる
Android で loadUrl すると onPageFinished のイベントが2, 3回呼ばれることがあり、それは毎回ではなくて時々そうなる。あまり行儀は良くないなと思いつつも anyOf()
というMatcherを使って渡したリストのどれかと同じならテストを成功するということにした。
expect(context.pageFinishedEvents, anyOf(equals([...]), equals([...])));
CI上で動かす時とローカルマシンで動かす時で動作が違う
これが一番ハマった。というかiOSに関しては解決していない。自分のMacで動かすとiOS, Androidともにちゃんと動くのだけど、CIで動かすとなぜか動作が違うことになる。
AndroidだとEmulatorに入っているChromeのバージョンが古いとWebViewの動作が違うので動作が変わるのが原因だった。最新OSのEmulatorはChromeのバージョンが新しいので最新OSのEmulatorを使うのがよい。
iOSは、XcodeのバージョンもOSのバージョンも一緒なのにWebVIewの動作が違うことがあって謎。具体的には、手元ではonPageStartedにイベントが来るのにCI上では来ない。一応、CirrusCIのサポートに問い合わせているのだけど実装が悪い説もあるとは思っていて困っている。
これも anyOf()
で対応するしかないかなと思いつつサポート待ち。
SIGSEGV で落ちる
Androidは時々これで落ちる。原因がわからないのでとりあえずRe-Runしている。悲しい。
テストに失敗すると途中で止まる
flutter driveはtargetを1個設定して、そこに記載されているテストを実行する。そして途中でテストが失敗すると途中でflutter drive自体が止まる。
最初は、webview_test.dart, webview_controller_test.dart などファイルごとにテストを作って、それらのテストを1つのtarget fileにまとめて実行していた。その場合、テストに失敗した時に途中で止まってしまうので、テスト実行 → テスト失敗 → 直す → テスト実行 → 別のところが失敗 → 直す → テスト実行 の繰り返しでテストを待つ時間が多かった。
なので、matrixを指定して、それぞれのテストファイルを別に実行するようにした。そのおかげで待つ事が減ったのとある程度並列で実行されるようになった事、あとはRe-Runの影響が小さくなった。
デメリットはテストファイルを追加したときに.cirrus.ymlも更新しないといけないこと。
処理が最後まで終わってからテストする
もともとは、onPageFinishedなどのイベントをStreamに流して、テスト側ではそれをlistenしてイベントが来るたびにテストをしていた。そしてあるイベントに来たタイミングで complete を呼び出して、テストを終了させていた。
この方法は、わざわざwaitする必要がないので比較的早いのだけど、処理の途中でテストが失敗することがあるので、そのあとどうなったのかわからなかったりする。integration test は1回の実行時間が長いので、実行回数をなるべく減らしたい。そのためには1回でどういうイベントが来るのかをすべて把握したかった。
なので、直近では処理が終わっているであろう時間までとりあえず待つ方式に変えた。ただやはり遅いので、waitの方法を変えるかもしれない。
iOS と Androidの両方で同時に動かせない
困っている。もう手元で動かさずにCIで動かす前提にするのがいいのかもしれない。
まとめ
大変だけど書かないといけないときは書くしかない。
ちょっと話は変わってflutter_test の話だけど、Matcherになにがあるのか把握出来ていないのでテストが書きにくい。
TypeScriptではMatcherに何があるのかわかってなくても expect(hoge).
とか打つと補完でなにがあるのか一覧で表示されるのでそれっぽいのを選んで書ける。