
Testimのサイトで紹介されていた「10 Rules for Writing Automated Tests」という記事をみつけたのでざっくり翻訳してみます。著者はTestimのCEOです。
10 Rules for Writing Automated Tests
最近、たくさんの顧客から「どうやって自動テストを作っていけばいいか」を聞かれます。パフォーマンス、楽な運用やスケーラビリティが重要なポイントです。今回、この問題についての対策を提案してみたいと思います。
以下のルールは自分たちのテストスイートを作るときに考えたものです。我々はドッグフーディングをとても信頼しています。そして、TestimはTestimを使ってテストをしています。
藤原メモ: mablも同じこと言っていたけど、QA自動化サービスでざっくりとした自動化を行い、細かい点はドッグフーディングを使うのが、現状のベストプラクティスなのかもしれませんね。よって、マニュアルテストを自動化していくのは効率も効果も悪くなりがち。
Rule 1: 優先順位付けする
たくさんのアプリは1000を超えるシナリオを抱えています。機能性や正確性のテストは高品質のプロダクト届けたり、ユーザの期待に答える唯一の手段でしょう。そして、パフォーマンステスト(通常のスピードやレスポンス性、Loadやストレス耐性の確認)、ビジュアルテスティング(通常は1+1=2のように足し算になるのが当然ですが、たくさんの見た目のバグは最悪な体験をユーザに届けてしまいマイナスになってしまいます)といったたくさんのテストが存在します。
まずはもっとも大切なユーザフローを洗い出しましょう。たとえば、ログイン、カートに追加する、購入するなどです。洗い出した一覧にたいして、ユーザが重要だと考えるかどうかで優先順位をつけていきます。
そして、その中から必ず成功してほしいものを洗い出します。たとえば、ユーザ作成、購入や支払いなどが考えられるはずです。Google Analyticsなどを使ってユーザに共通するシナリオを洗いだすこともできるはずです。Segment.ioもとても便利なツールです。
Rule 2: 削減!リサイクル!再利用!
次に、ユーザシナリオをシンプルに、ひとつの目的だけ持つように、最小の単位でブレークダウンします。たとえば、「ユーザ作成」、「ログイン」、「メールを送る」などです。それぞれの流れをもとにテストを作成します。完了したら、ユーザストーリーと比較してみましょう。
命名規則: だれでも理解できる命名規則を作るのはとても重要です。たとえば、一連のステップの流れ(フロー:Flow)の名前には対応するコンポーネント名(機能名)をいれてもいいでしょう。これらによってフローの構造が決まります(例: 「Account.login」や「Account.logout」のように「分類名.機能名」とするとわかりやすいです)
コンポーネントの再利用:再利用をしましょう。経験の少ない開発者やテスターはコピー&ペーストを使って再利用しようとします。しかしながら、ステップが更新されると同時に修正が必要になります。たとえ1ステップのコピペであっても、繰り返される回数が増えるととても大きな苦痛になってしまいます。
藤原メモ: 再利用できるコンポーネントを増やしすぎると、逆にメンテナンス性が損なわれます。よって、ソースコードと同じでどの粒度で再利用化すれば自分たちにとってハッピーかを考えるべきでしょう。
Rule 3: テストはひとつの目的だけ持つ構成にする
テストはひとつだけの目的を持ち、ひとつだけバリデーションするだけ!にしましょう。
ひとつだけの目的を持つテストは、簡単に作成でき、メンテナンスも楽ちんです。長いテストや複雑なテストと比較しても、実行時間は短くなります。理想をいうならば、テストしている機能や依存関係に問題があった場合に、失敗するテストを最低限におさえるべきでしょう。
藤原メモ: さらに並列化によって同時に並行実行できるため、小さいケースが増えても実行時間はそれほど長くなりません。たくさんのものを並行して繰り返し実行は、機会がもっとも得意とする作業です。ルール10参考のこと。
テストは3つの部分で構成しましょう。
- セットアップ(例:ログインなど): テストに必要な処理(AUT :application under test)を行います。あるテストはログインが必要だったり、あるテストは別の何かが必要だったり、処理はテストによって異なるはずです。既存の複雑な処理を実装します。NOTE: 最適化を急ぐとろくな結果になりません。たとえば、ログインフロー(ユーザ名とパスワードを入れてクリック)のように簡単なフローから実装するといいでしょう。そうすれば、作ったフローを他のテストで再利用していても、修正は簡単にできるはずです。最初から、ログインで利用するCookieをごにょごにょがんばって処理速度を最適化しようとする必要はありません。
- アクション: テスト部分です。前述のログインフローが成功したあとにつづくステップを書きます。注意したいのは、ログイン自体のテストは、別のテストを用意して、再利用すべきではないです。
- バリデーション(妥当性): UIの状態やデータを確認します。これらはテスト作成者が予測しているものになるはずです。たくさんの異なるバリデーションが可能ですが、「週次で本番データが更新されていることを確認したい」や「テストデータと同じであることを確認したい」といった予測(目的)によって選択します。UIテストにおいては、以下のような要素に対する確認が中心になります。
- 要素が見えていること
- 要素が期待するテキストを含んでいること
- 要素が特別な状態になっていること
- Visual Validation。テキストや画像が正しいスタイル(フォントサイズやレイアウト)で表示されていること
セットアップの部分で、事前にデータセットを用意しておけば、バリデーションをより簡単に実行できるようになります。ステップが完了したあとに期待する値を用意しておくこともできます。
ただし、データセットが更新された場合、予想とは違ったシナリオになるはずです(別のユーザが違った方法でアプリを操作するのと同じように)。これはとてもデバッグや再現が難しくなります。
ログインを例として見てみましょう。
- セットアップ(初期状態設定)
- テストに必要な処理を実行
- 初期登録ページに遷移する
- アクション:
- ランダムなユーザ名を選択し、変数usernameに設定する
例: “user” + Date.now() + “@mailinator.com” - ランダムな文字列でパスワードも変数passwordに設定します
- 変数usernameを使って初期登録します
- ユーザ名に変数usernameを設定します
- パスワードに変数passwordを設定します
- 登録ボタンをクリックします
- バリデーション
- メールを開きます(例: mailinator.com など自動テストからアクセスできるものが便利でしょう)
- 受信ボックスを開きます
- ユーザ名が書かれたメールを探します
- メールが1通みつかり、メール内にアカウント有効化するためのリンクがあることを確認します
- メール内のリンクをクリックします
- ユーザ名とパスワードでログインします
- 「ログイン成功」と表示されるのを確認します
- ランダムなユーザ名を選択し、変数usernameに設定する
NOTE: ユニットテストとE2Eテストの大きな違いはテスト実行のオーバーヘッドです。ブラウザを開いてセットアップして・・・は以前はとてもとても時間がかかっていました。テスターはセットアップ状態をひとまず作り、そのあとにたくさんのテストを実行することで時間を節約していました。しかし、最近ではDockerなどの技術によりブラウザ起動が高速化され、素早くアプリを起動できるため、オーバーヘッドは小さくなってきました。
Rule 4: テストの初期状態には一貫性をもたせる
自動テストはつねに同じステップを繰り返すため、同じく同じ初期状態からテストを開始すべきです。テスト実行時における共通かつ最大のメンテナンス問題は、初期状態の一貫性を保証することです。自動テストはデータの状態に依存するため、一貫性がない場合、結果の一貫性もなくなります。初期状態はたいていの場合、ユーザの直前の動作に影響を受けます。以下に例を挙げます
- すでに商品がカートに追加されている(前のテストの影響)
- 画面表示やふるまいに違いがある(新規ユーザとすでに存在するユーザで違う動作をする影響)
- ログイン画面に遷移せず、ログイン後の画面に遷移してしまう(ユーザが既にログインしていた場合)
解決策は以下です。
- テスト実行時に毎回ユーザを新規作成する
- 誰かが利用するテスト環境とは別に、テスト自動化用に専用環境を用意する
- テストスートの実行前にアプリとデータを初期化する。有名所でいうとfixturesのような方法がある
- データの設定が難しい場合は、条件分岐を入れて複数のケースをうまくきりわける
- テスト間の依存関係を取り除く
Rule 5: シンプルなステップを使って複雑なテストを書く
現実のユーザのシナリオを真似ると複雑なテストになりがちですが、その場合、シナリオ全体を実直に実装するのではなく、シンプルなステップで部品を作り、それを再利用してテストを構成しましょう。部品はすでにテストされて様々な場所で使われているため高品質のはずです。これらの部品を使って複雑なテストを作り失敗した場合は、部品の結合の問題だけにしぼりこめます(ルール2も参考のこと)。
まずはシンプルなテストから順番に実行されるようにしましょう。テストスイートを実行するときに、複雑なテストなどで何か問題があった場合、すぐに問題にきがつけるはずです。
Rule 6: 大切な場所にバリデーションを追加する
バリデーションは、テストが成功したか失敗したか確認できるように、通常、ひとつのテストの最後のステップとして利用されます。また、アクションが失敗した場合に、チェックポイントとしてふさわしい場所にバリデーションを追加するのも大切です。同期処理に時間がかかっている場所では、バリデーションは次のステップに進む前に、タイムアウトが起きるまで待ってくれます。以下例です。
- ひとつのアクションが完了するまで待つためのバリデーション。画面遷移、フォームのサブミット処理、データの表示処理など
- DOMの変化を待つためのバリデーション。変化する場所にバリデーションを入れることで、新しい状態の確認を行ってから次に進めます
- クライアントとサーバ間の連携を待つバリデーション。データ登録成功メッセージなど、サーバからのレスポンスによってアプリの状態変化を確認します
Rule 7: 安定性を改善するためにスリープを入れない
いろんな時間を待つスリープを入れてしまうと、不安定なテストになってしまいがちです。ほとんどのアプリは非同期で動くのが当たり前になってきましたが、次のアクションにっすうむ準備ができたことを予知できないレアケースも存在します。これは自動テストにおいても同様の問題です。
スリープを避ける理由は、テストを実行したときの環境負荷があまりわからないからです。パフォーマンステストの場合、CPUやメモリを専有してしまいますが、Dockerのような仮想環境の場合、長いスリープのためにタイムアウトが発生してしまったり、スローテストになってしまいます。
ほとんどの場合、人間が実際にやっているように、とある状態が変わるまで待つ・・・といった条件付きで待つステップを使えば解決できます。
Testimでは「wait-fors」という機能を提供しています。これによって、要素が表示されるまでや、テキストが期待するものになるまで待ちます(変数や正規表現も利用可能です)。さらに見た目の変更を待ったり(色の変化など)、コードレベルの変更も検知できます。
Rule 8: 抽象化は最低2段階ぐらいにする
テストのほとんどに、クリックやテキストの設定といったユーザインタラクションが含まれている場合、なかなかテストがうまく動作しない場合があります。Seleniumのようなローレベルのフレームワークを使っている場合、PageObjectデザインパターンのようなビジネスロジック(たとえばログイン)、と操作(たとえばユーザ名設定、パスワード設定、クリックログインボタン)の2段階に切り分けるのが推奨されています。
Testimでも同様のことをしています。階層に分けることで、再利用やムダの削除となり、メンテナンス性が向上し、開発者でもテスターでも、誰でもこのテストによって何が起こるか? 簡単に理解できるようになります。開発者の場合を考えると、このテストはどういったテストなのかが理解できるため、バグが何なのかも簡単に理解できるようになります。
Rule 9: 条件分岐を減らす
テスト内で条件分岐を利用すべき箇所があります。しかし、多用すると予期せぬこことになったり(どれが正しいのかわからなくなったり)、複雑になったり(予期せぬループに陥ったり)するので注意が必要です。対策として以下が考えられます。
- 初期状態を設定してからテストを開始する
- ポップアップする部分を止めてしまう
- ABテストや特別なフローを止めてしまう
Rule 10: 独立した、分離されたテストを書く
テスト自動化における重要な方法論は、テスト自体を独立したフローで作成することです。これによって並行実行が可能になり、テストスイートが圧倒的にスケールするようになります。仮に1000ものテストが存在し、それぞれが1分かかるとします。並列実行すれば1分ですが、直列だと16時間かかります。
高速なテスト実行時間により、フィードバック時間(ここでは実行結果を確認するまでの時間)も短くできます。開発者がコードをコミットしてから、その機能がリリース可能状態になるまでの間の時間が減るため、真のアジャイル開発に近づけるのです。
まとめ
適切なテスト自動化計画は、開発プロセスに置いて大きな違いを生み出します。すばやいフィードバックを得たり、すばやく新しいテストを適用したり、素早く結果を分析したり、すばやくバグを特定し解決するための情報を素早く手に入れたりできます。
適切なテスト自動化計画は、時間や金銭的な支出を削減できます。適切じゃない場合、メンテナンスコストが高くなり、開発チームが新機能開発したり、テストカバレッジを上げる時間を奪います。
最後に、適切なテスト自動化計画は、品質に対する責任範囲を広げます。QA組織を超え、他の組織にいるメンバーまでもが品質に貢献してくれるようになるでしょう。