技術の発達によりテストのツールチェーンが安定してきている

古川陽介氏(以下、古川):前までは、HTML出力されて、さらにブラウザがどう解釈したかが出力されるという2段構えになっていました。

そうすると何が問題かというと、ブラウザのバージョンや、読み込まれるタイミングによって、ピクセルのズレみたいなものが若干発生した時に、閾値でなんとかしていたけれど、若干のやりにくさが感じられていたんですよね。

そこに、Reactのツールが増えてきて、さらにスナップショットテスティング。真面目にテストのデータを逐一見ようとするから、難しいんだよ。ある程度ダンプしておいて、その内容が完全に一致していたらいいんじゃないの、もしくは、「ちょっと間違っているぐらいだったらいったん出しておくけど、それをどうするかはそっちで決めて」くらいの柔軟性を持たせたテストができるようになってきたところが、やはり強かったんじゃないかなと僕も所感として持っています。

倉見洋輔氏(以下、倉見):あとは、先ほど古川さんがちょこっと言っていたピクセルのズレとかバージョン差分は、たぶん今ではあまりそこまで気にするほどの問題ではないと思っていて。

フロントエンドとバックエンドが分離され始めたいわゆるReact黎明期は、オートメーションで動かせるブラウザがかなり限られていたんですね。

古川:あまりなかったですよね。

倉見:「PhantomJS」、みんな名前を覚えていますか(笑)?

古川:もちろん覚えていますよ(笑)。

倉見:PhantomJSって、例えばFlexboxを描画できなかったのをご存じですか?

古川:知らない(笑)。

倉見:かなり致命的というか、それこそCSSをきちんと書こうと思った時に、5、6年前の時点でFlexboxすらまともに描画できないブラウザって、なんかもう使えるとは思えなくて、やはり選択肢から除外していたんですよね。

そこに、例えばChromeとかが内包しているレンダリングエンジンを再活用する技術。「Electron」とか「NW.js」といったものが到来したり、その部分だけを切り離して高速に動かす、Chromeの「Headless Chrome」と呼ばれる技術ですね。

古川:「Puppeteer」。

倉見:Puppeteerが有名ですが、そういったものを、フロントエンドだとlockfileと呼ばれるパッケージの管理にnpmとかを使っているものと併せて運用することによって、ふだん自分たちがよく使っているレンダリングエンジンのバージョンを固定した状態でテストのツールチェーンとして使うことができるようになったというのが、たぶん3年、4年前ぐらいから来ています。

やはり同じブラウザを使ってテストしていると、ピクセルのズレは、本当にほとんど起きないんですよ。どうしようもないレンダリングパイプラインの中のポイントみたいなのはあるのですが、かなり稀なケースと言ってもいいぐらいですね。

古川:それで言うと、ツール面もけっこう安定してきているということですかね。ブラウザをヘッドレスで動かすツールとしても、Puppeteerなどをうまく使うというのが、バージョン固定するとかそういう技術も含めてけっこう安定してきている。

倉見:そうですね。だから最初は文字列という、機械が扱いやすいものをベースにやっていましたが、それだけじゃない選択肢ももう僕らは手に入れているという考え方ですかね。

ブラウザで動くService Workerをベースにしたテスト用のツール「Mock Service Worker」

古川:フロントエンドからした時の出力結果を確認するフェーズのところのツールは、先ほど言ったHeadless Chromeとか、ヘッドレスブラウザ系で確認する。もしくはスナップショットテスティングとかでダンプしたオブジェクトが同じかどうかを確認するというのが、出力した結果を確認するベースでは今やられていることですが、先ほど和田さんのところであったナローポイントと言っていたところのデータを入力として扱った時に、入力がしっかりしていれば、出力を関数として扱うことができるとお話がありました。

入力側が疎結合になっているとはいえ、どうするのが今ベストというか、ツールとして何を使うのが一番わりとよくあるものなんでしょうか? 僕の聞いている話だと、「MSW」とかですけど。

倉見:そうですね。ここの3人って、リクルートの社内の定例で毎週顔を合わせて、「こんな技術に今興味を持っている」という話をしていますが、僕というよりは、その中のほかのメンバーが、けっこうテストにMSWを使っているって(言っています)。今、ちらっと古川さんがおっしゃってくれたMSWは、「Mock Service Worker」の略称のツールです。

古川:ノーヒントで言っちゃったな(笑)。Mock Service Worker。

倉見:Service Workerと付いているとおり、ブラウザで動くあのService Workerですね。あれをベースにしたテスト用のツールという、ちょっとおもしろいやつなんですけど。Service Workerには、HTTP通信を乗っ取るみたいなことが機能として用意されていて、それをナローポイントを握り込むのに使ってあげようという思想なんですね。

なので、XHRとか、かなり通信に近い状態でスタブというかモックというか、することができる。

フロントのアプリケーションを書いている中で「なんとかHttpClient」みたいなクラスを自分で作って、そこを握り込もうとする時に、その中のロジック、例えばパスや、HTTPステータスのエラーハンドリングコードのハンドル同期が間違っていたら、結局、致命傷になってしまう。やはり、フロントエンドというのは、そこも通してテストされるべきなんですよね。それをやりやすくしてくれているという良さがあります。

あと、Node.jsでも動くので、サーバーサイドでもフロントエンドでも両方でJavaScriptを使いたいみたいな、僕はけっこうそういうタイプなんですが……そんな人間にとってはすごくありがたいツールで、最近は、期待のおすすめツールという感じです。

古川:そうですね。プロセスを1個も立ち上げなくていい。今までのやつは、Node.jsでモックを作ったりとか、そういうのが多かった。

もしくはそうじゃない場合、今度は自分たちでレスポンスの戻り値っぽいJSONのオブジェクトをあらかじめ定義しておくという感じでした。だけどそれはそれでそれの管理どうするんだみたいな話にもなりがちで、一応ちゃんとしたXHR……XHRって言っていいのかな。Fetchの通信が行われて、その結果としてその戻り値が返ってくるというところが1つありますね。

あと、僕ずっとナローポイントって言っていましたけど、和田さんから突っ込みが入って、「ピンチポイント」ですね(笑)。

和田卓人氏(以下、和田):ごめんなさい(笑)。

古川:いえいえ、ぜんぜん大丈夫です(笑)。

ちょっと思ったのは、やはり先ほど言っていた通信部分ですよね。Mock Service Workerにせよ、モックにせよ、通信のところを単にモックというもので表現したほうがいいのか。

それって、こっちからの期待もあるじゃないですか。こういうのを返してねという期待だったり、いわゆるそれってスペックにも近いんですよね。仕様にも近いというか。「こういうデータ構造でこういうものを返してほしいです」というのを、モックとして記述する。そうではなく、モックとして記述するというところからスタートさせて、それを期待として書いていくというのが、Mock Service Workerのアプローチだと思います。

もしくは、もうRESTなAPIとしてOpenAPIみたいなかたちで、「Swagger」とかを使って仕様から先に書いて、それを元にモックを作るというアプローチもまたあると思います。

大事なのは、妄想と本物で動きが違うということにどれだけ早く気づけるか

古川:全体を見た時に、そういうアプローチがいろいろあるとはいえ、今、和田さんはどういうふうにするのがいいと思いますか。すごく雑な質問をしてもいいですか(笑)?

和田:どういうふうにするといいというよりは、大事なのは、バックエンドとフロントエンドでよりどころにするスキーマのようなものがあること。

それを基にして合意を持つ。こういうリクエストが来たら、こういう意味なので、こういうレスポンス返してくださいというのが、一種の約束の場となる。つまり、スキーマを真ん中において、バックエンドとフロントエンドが共通しつつ並列して開発ができるようにすることが大事です。

真ん中のスキーマに何を使うかは、状況によりいろいろ異なりますよね。GraphQLの仕組みを真ん中に置く人もいます。

また、CDCテスティング、Consumer-Driven Contracts testingという仕組み。古くは「Pact」という仕組みから端を発したバックエンドとフロントエンドの間のやり取りをスキーマとして表現するだけじゃなく、お互いがどういうことを期待しているというのを、ある程度テストとして生成するような仕組みを備えたり。

あとは、まさにSwaggerみたいなものもそうだし、スキーマを書くだけじゃなくて、コードも生成して、テストも一応生成して。大事なのは、考えていることが(実際と)違うということ。要するに、自分たちが考えたモックやスタブというのは妄想なので、妄想と本物の動きが違うということに、どれだけ早く気づけるかがとても大事になってくるんですよね。

古川:なるほど、なるほど。期待と実際のギャップというか。

和田:だからその意味だと、フロントエンドの開発は、やはりTypeScriptとかを使って、静的解析も一緒に備えていくのがよくて、早いタイミングで、「なんか考えていることが変わったな」とか「プロパティが1個生えたな」とか「減ったな」とか、そういうのも含めて、すぐに気づける。

だから、CIを一緒に回していくのも大事になる。話は戻るけど、先ほどのテストのバージョン固定ができるようになったり、閾値を使って厳密な位置じゃなくても正誤判定ができるようになったりという技術が全部一緒になった結果、フロントエンドとバックエンドのテストがどちらもCIで回せるようになって、かつ、フロントエンドとバックエンドの間で考えのズレが生じた時に、早めにそれが検出できるようになった。コンパイルエラーのレベルで検出できたら一番早いですよね。

そういうかたちで、多段防御みたいな。最後にくっつけて、初めて「あっ、なんか違った」とわかるのではなくて、さまざまなレベルで備えが張ってあって、かつ、最後まで行かなくとも軽微な間違いは、その前の網で捕らえられるという構造が作れるようになってきたというのが、やはりフロントエンドのテストの進化ですね。

(次回へつづく)