UIコンポーネントの自動テスト
Webフロントエンドのテストについて最近いろいろ考えていることをまとめます。
書きやすいテスト/書きにくいテスト
書きやすいテストでまず思いつくのは関数の単体テストで、副作用のない純粋な関数なら引数を条件に戻り値を確認するだけ。
でも実際のアプリにはこうゆう関数はあんまりなくて大多数は副作用が多く複雑なオブジェクトの集合体です。
しかも事をもっと複雑にしているのは時間。つまりサーバーサイドと違ってフロントエンドはユーザーがブラウザでURLを入力してページが表示されてから速やかに操作するのではなく、少し間をおいて操作したり、APIの応答を待たされたりする。
そしてJavaScriptの世界では非同期処理が多く使われるので単純にコードが上から下へ順に呼ばれるとは限らない。だからUIが絡むテストはとても書きにくい。
でも書きやすいUIコンポーネントもあったりする。例えばReactのStateless Functional Componentなんかは文字通り状態を持たないから、純粋な関数そのものなわけです。
しかしですよ、そんなコンポーネントのテスト書いても対して効果なくないですか?だって静的なHTMLがそのままの状態であるかどうかなんてテストして確認するまでもなくそのままの状態ですよ。やりたいのはそうゆうことじゃなくて、いろんな条件で変化する状態をちゃんとUIに反映して、ユーザーの操作に反応できるのかってことです。
とは言っても条件分岐するならそれはテストしておきたい…なのでテスト対象から外すことはもちろん出来ません。
テストする範囲を少し大きくする
だから状態を持たない小さなコンポーネントをテストするんじゃなくて、その親に対してテストを書きます。これはもうユニットテストという概念は一旦忘れましょう。単体ではなく複合した状態でないとテストする意味が薄いからです。
そうゆう意味ではインテグレーションテストなのかもしれない、かと言ってE2Eではないです。確実に。アプリをビルドしてブラウザを動かして、ということはやってないから、あくまでロジックのテスト。
それからUIのバグって大体コンポーネント間のインテグレーションにあるんですよ。なぜなら繋がりはコードに表しにくいから。例えばユーザーが「フォームに入力をして、送信する前に一回キャンセルして、またフォームに戻ってきた」という操作が出来る場合、コンポーネント間の連携はちゃんと出来るのか?それをテストするならその親を見るしか方法がない。
Googleの自動テストの分類がいい感じ
Googleでは今までの一般的なテストの分類方法は、曖昧で混乱しがちなのでそれは辞めて、データに基づいた分類をしているようです。
Smallは一見ユニットテストに見えますが、でも関数一つに縛られているわけではないです。あらゆる連携を取り払って意味がある単位で括って60秒以内に終わればそれはSmallテストというわけです。
でもAPIはモックしたとしても、フロントエンドでもLocalStorageを使ったりするわけで、そうゆう意味ではUIのテストはMediumに含めるのがいいのかなと思う。
じゃあどうやるのか
ユニットに拘らなくてもいいんだ!という免罪符を手に入れたところで実際にどう書いていくのかですが、とにかく子コンポーネントと関わる機能(API呼び出し以外)をたくさん動かす。例えばVueだったらshallowMountは使いません。ReactだったらContext APIのProviderも動かします。
ReduxとかVuexも必要な分だけ初期化して使えるようにしてあげます。
そもそも親コンポーネントがテスト対象とわかっているなら、状態のハブになるように書けるのではないでしょうか。それはもうPresentational and Container Componentsの概念なのではないかと思います。
Containerにロジックを含めてその子供達は表示に専念する。そうすることでUIは純粋関数に近づいていくというReact hooksの作者Dan先生が提唱した設計手法ですね。
というわけで
どう振る舞うかを知っているコンポーネントが、渡されたデータをどう表示させるかを知っているコンポーネントを操る。なので、どう振る舞うかをテストすれば意味のある単位でUIのテストが書けるようになる。これが僕の最近の結論です。