はじめに
8月末と11月前半に参加した、**「LAPRAS公開設計レビュー t_wadaさんにこのテストでサバンナを生き抜けるか聞いてみた」**という勉強会がとても面白く参考になったので内容を抜粋してまとめてみました。
E2Eテストについて
End to Endテストとは、システム全体が正しく動作することを確認するもの。
具体的には「利用者によるWeb ブラウザ等の操作により、想定通りの動作となっていることを確認すること。
既知のcheckは機械にやらせ、未知の動きを発見することに人間のリソースを割くようにすることがポイント。
つまり、こんな動きするユーザーいるかも、という動きを発見することに人間の目を使って手動でテストをする(またはテスターさんにテストをしてもらう)。そして正しい動きや容易に考えうる異常な動きは全て自動でcheckできるようにしておく。
そして、人間により発見した未知の動きが出てきた時に、それが既知になり既知をカバーするテストを書く。
このサイクルを回すことでテストが資産になってくる。
まさに陣取りゲームのようなもの
検証環境の理想的な使い方
上述したように自動化できるcheckは全てE2Eテストやその他テストでカバーできるようにしておき、これらがpassした時に検証環境にデプロイされるような環境を整えておくのがまず前提として必要。
自動テストでは検出できなかったけど、検証環境、または本番環境で初めて出てきたバグを監視できる仕組みを整えておく。
検証環境や本番環境でのバグを全て避けようとすると、テストを書くのが重すぎるので良いバランスを保つのが良い。
継続的な監視とエラー率を検出して、一定の値を超えたときに自動ロールバックしてリリースを取り消すことができる仕組みづくりが必要。
デプロイの自動化だけではなく、後戻りを自動化する技術とのワンセットになっていることが大事。
E2Eテストの弱いところ
-
E2Eテストはflakyさが高い = 実行結果が不安定
- 外部サービスが落ちていたらこちらのテストが落ちたり、ブラウザに依存したり。
- 依存で言うと、data.nowなどでシステムの時間にアクセスするのも外部依存であるので意識しておいた方が良い。
-
ユニットテストを少なく、E2Eテストはカバー範囲を無理に広げようとすると、flakyさが高まるのでユニットテストで安定的なテストを書くことがとても大事。
-
テスト毎の失敗、成功の履歴などをデータとして保存しておくと、どのテストがどれぐらいflakyかを見つけることができる
- テスト毎に見つけやすいtagをつけることができる (多くのテスティングライブラリにそういったtag機能が付いている)
- 最初は手動でも良いので、そういうタグをつけると良い。そうすることで、それぞれのテストのテスト通過率を出すことができる
結合テストと単体テストで同じようなテストを書いてしまうのを防ぐには
例外系のテストはユニットテストのレイヤーでは細かくやる、Featureテストなどでは正常系のみやるみたいなルールを定めると上手くいくことが多い。
結局どういう構造でテストをやれば良い?
-
カバー範囲と安定性はトレードオフではない。両方取ることができる。
ユニットテストを充実させて安定性を高め、E2Eテストもできるだけ書いてカバー範囲を広げる -
大体3レイヤーのテスト構造がおすすめなことが多い。
- 例としては、モデルの細かいテスト、API単位の結合テスト、E2Eテストの大きく3つに分けるなど。
フロントエンドのテストに手をつけられていない状況を突破するには
そもそも後からテストを書くのは難しい。
なぜならテストの書きやすさを意識せずにどんどんプロダクトコードのみを書き続けられてきたから..
テストを書くためにはプロダクトコードに手を入れないといけない(リファクタリング)。
だからテストを書くことを前提にせず突き進んできたプロダクトコードに対していきなりユニットテストなどの細かいテストを書くことは難しい。
しかし突破することはできる!
以下のような手順がオススメ。
-
まずは大外のテストをして包む (ここは正常系のテストのみをやる)
最初は無理をしても強引に大外からテストを入れる- E2Eテストで、バックエンドも含めたエンドユーザーから見た一連の動作をテストする。動作は遅く、Flakyなので開発をドライブするテストにはなりにくいが、どれだけ中身のコードが汚くても、見た目をとりあえず検査するテストは書ける。
- ビジュアルリグレッションテスト (見た目のレベルで何かが変わったことを検知するテスト、画像のdiffを撮るとか、画像の差分を洗い出す技術はここ数年で発達した)を行うのも良い。jestのDOMの差分取るテストもここにあたる
- どうあるべきかはもう年月が立ち過ぎてわからないけど、今どうなっているか、どこをいじったらどこが変わるかを記録して現状を把握する (as isを把握して、to beは一旦捨てる)
- 優先順位としては影響範囲が広い機能、バグるとユーザーに与えるリスクが大きい機能から行う
- または不具合修正のタイミングで見た目のテストを書きながら、修正をするというのも良い
-
リファクタをしても、見た目が変わらないかを確かられる状態をE2Eテストなどで作ることができたら次のステップ!
-
フロントエンドのみのインテグレーションテストを行う
バックエンドはモックして、フロントエンドのみの結合テストを行う -
ユニットテストを行う
- ロジックを切り出してテストを作る (接合点を作る)
- moduleごとのテストを行う
- 切り出せたらそれぞれに対して正常系と異常系のテストを書く
仮説検証のための新規プロダクト開発において、テストはどの粒度で書くべきか
ごく少人数で作る場合(1、2人)などは特に、慣れているスタイルで良い!
構成要素を積み上げながら細かいテストを書いていくスタイル、または外から包み込んでいくようにテストを書いていくスタイルか、慣れている設計スタイルによって変わる。
仮説検証のための新規プロダクトの場合、実装方針が大きく変わる間はユニットテストは無駄になるかもしれない可能性が高いので、リクエストベース(APIのフィーチャーテスト)のテストを分厚く書くのは良い!
だから最初にユニットテスト書き過ぎはコスパはよくないかもしれない
でもコードの良し悪しはユニットテストによって押し上げられるので最低限は絶対に必要
新規開発の際に意識すべきこと
設計 = テストを書くこと
設計がちゃんと固まらないとテストが書けないは大きな認識の間違い!!
テストコードを書いてみて、初めて自分は何が作りたいかを知る
- コアのドメインモデルにはユニットテストを書きながら実装を進める
- テーブル設計などは可能な限り早期に決めるようにする
- フレームワークを使うと、コードの結合度は増すというデメリットはあるが、フレームワークに依存しつつ比較的良い設計を意識していくと良い。例えばRailsでクリーンアーキテクチャをやろうというのはRailsの良さを活かせていないとか、どうフレームワークを生かすかを考えないといけない
- これを考えられる人が上手くRails等のフレームワークを活かせるので、実はRailsなどって中上級者向きで、全然初心者向きではないかも
早く進むのが偉いというフェーズと、マラソンのように長距離を走らないといけないというフェーズで取るべきアーキテクチャがもちろん違ってくる
e2eテストのメンテナンスについて
-
AutifyなどのAIを使用したサービスを使用することで、テストの自動修正が可能になる
- AIがリリースの度に変更されるUIの変化を監視し、テストシナリオを自動的にアップデートしてくれ、壊れたテストスクリプトをひとつひとつ直す作業が不要になる
-
もしくは変更に強いE2Eテストを書く意識を持つ
テストを消す基準について
同じようなことをしている(重複している)テストをどう探して、どう消すか、過剰なテストをどう消すか
理想は、gitでhistoryを見て、書いた当人に聞いたりすること。
でも...人は自分がなぜそのようにプログラムを書いたかのかを忘れがち..
そうならないために普段からテストの意図を残すことを徹底する。
gitのコミットメッセージに残したり、Pull Request内にIssueなどへのlinkを残しておいたりする。
テスト間の行レベルでの重なりは機械的には調べることができるが、まだまだテストの被りを機械的に自動で洗い出すことは今の技術では難しい...
ペアプロなどでダブルチェックしながら、消したりするのも良い
またテストのツリー構造のメンテナンスをすることは大事。
jestのdescribeの中のdescribeを綺麗にするなど。
モックを使ったテストについて
開発マシンに入るものは本物を使う、入らないものはモックするという基準をt_wadaさんはオススメしているそうです。
モックを多用すると、自作自演になりがちなので...
ロンドン学派
- テストファースト&モックを良く使う (設計を導き出すために、モックを実装より先に使う)
- 代表的な書籍「実践テスト駆動開発」
デトロイト学派 (Kent Beckから始まる流派、 t_wadaさんはこちら)
- テストファースト&本物のオブジェクトを複数使うスタイル
- 代表的な書籍「テスト駆動開発」
モックはテストしにくいところにテストをしやすくするための道具ではない!!
テストがしにくいということは、そもそも設計が悪いので、モックを使わずに設計を良くする方向に進めると良い。
認証APIや決済系のサービスはローカルに持ち込めないので、モックを使ってもしょうがない。
モックを使うよりは、テスト用の別実装、フェイクオブジェクトの方を使うべきとの考え。
この辺りの詳しい歴史的な流れなどは
「テスト駆動開発」の「付録C」に書いているらしいです。
あとこのスライドにも書いています。
その他
-
破壊的な変更は段階に分けて行うことを意識することが大事。
- 特にDBの変更など。後戻り可能な設計をすることが最も大事だが、どうしてもできないことがある。そういう際に破壊的変更を何段階かに分けて実装、デプロイすることができないか検討すると良い。