テスト自動化のベストプラクティス

# はじめに
テスト自動化は比較的新しい技術であるため、参考資料が少ない領域です。
そのため、どのようにテスト自動化をすればいいのかわからない方もいらっしゃると思います。

この記事では、テスト自動化つまりSET(Software Engineer in Test)の領域についてアンチパターンとベストプラクティスを含めた概要と、E2Eテストフレームワークの[Cypress](https://www.cypress.io/)を用いた実践例を紹介します。

「どうすれば、テスト自動化が成功するのか？」について詳しく書きましたので、最後までお読みいただければ幸いです。

# この記事の対象者
- SETエンジニアである方
- E2Eテスト自動化に興味があるフロントエンドエンジニアである方
- テスト自動化の全体像を知りたい方
- テスト自動化がどうすれば成功するのか知りたい方
- E2Eテストフレームワークの[Cypress](https://www.cypress.io/)について深く知りたい方

# テスト自動化をする目的と役割と職種
## 目的
テスト自動化をする目的は、**リグレッションテストの自動化による工数の削減**です。
## 役割
[ソフトウェアテスト自動化の教科書 〜現場の失敗から学ぶ設計プロセス](https://www.amazon.co.jp/%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%E3%83%86%E3%82%B9%E3%83%88%E8%87%AA%E5%8B%95%E5%8C%96%E3%81%AE%E6%95%99%E7%A7%91%E6%9B%B8-%E3%80%9C%E7%8F%BE%E5%A0%B4%E3%81%AE%E5%A4%B1%E6%95%97%E3%81%8B%E3%82%89%E5%AD%A6%E3%81%B6%E8%A8%AD%E8%A8%88%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9-%E6%9E%97-%E5%B0%9A%E5%B9%B3/dp/4297117363)の書籍を参考にさせていただきました。
### アンチパターン
- 一回だけ作れば良く、改修予定のない案件にテスト自動化を実装すること → テスト自動化は繰り返し実施することで真価を発揮するので、一回だけ作れば良い案件には不要だからです。
- バグがある可能性のある部分にテスト自動化を実装すること → テスト自動化の実装時点では、バグがないことが前提だからです。(テスト駆動開発を除きます。)
- 仕様の変更が多い部分にテスト自動化を実装すること → 変更されるたびに修正を何回もしなければならず、工数が増加してしまうためです。
### ベストプラクティス
- リグレッションテストにかける時間を人間よりも高速にテストできるコンピュータの利点を活かすことで削減して、その浮いた時間を有意義な活動(これまで人間がやりきれなかったテストをするなど)に充てること
## 職種
### SETエンジニア
- テスト自動化エンジニアとも呼ばれます。
- テスト自動化のプロフェッショナルです。
- QA(Quality Assurance)と開発エンジニアの中間領域ですが、どちらかというとややQA寄りの職種です。
- SETエンジニアの立ち位置は、QAでチームとして活動、フロントエンジニアでチームとして活動、部署として独立など、企業によってバラバラです。
- SETエンジニアとして専門の職種を置かずに、QAや開発エンジニアが余力のあるときにテストコードを書くときもあります。(補足: システム上で実際に呼び出され動作するコードのことをプロダクションコードと呼びます。開発エンジニアが書くコードです。)
- 本来の役割ではないかも知れませんが、ユニットテストのコードも書く企業が存在します。
- QAからSETエンジニアへのキャリアチェンジよりも、開発エンジニアからSETエンジニアへのキャリアチェンジの方が多い印象です。
- まだまだ社会的に知られていない職種なので、あなたが企業でオンリーワンになれる可能性大？

# テストタイプ
[フロントエンドのテストは皆のためのもの](https://postd.cc/frontend-testing-is-for-everyone/)の記事を参考にさせていただきました。
## 主要3タイプ
### ユニットテスト
- 別名: 単体テスト、コンポーネントテスト
- 範囲: 関数またはメソッドが意図したとおりに機能するかをテスト
- ツール: [Jest](https://jestjs.io/)、[Mocha](https://mochajs.org/)、[Cypress](https://www.cypress.io/)(現在、ベータ版) など
- テスト手法: ホワイトボックステスト
- 責務: 主に開発エンジニア、次点でSETエンジニア
### APIテスト
- 別名: 結合テスト
- 範囲: 関数またはメソッド間の相互作用をテスト
- ツール: [Postman](https://www.postman.com/)、[Cypress](https://www.cypress.io/) など
- テスト手法: ブラックボックステスト
- 責務: 主にSETエンジニア、次点でバックエンドエンジニア
### E2Eテスト
- 別名: UIテスト、エンドツーエンドテスト、エンドユーザ目線のテスト、UIの機能に着目したフロントエンドのテストコード
- 範囲: Webブラウザまたはネイティブアプリとユーザーの相互作用をテスト
- ツール: [Puppeteer](https://github.com/puppeteer/puppeteer)、[Selenium WebDriver](https://www.selenium.dev/ja/documentation/webdriver/)、[TestCafe](https://testcafe.io/)、[Nightwatch](https://nightwatchjs.org/)、[Cypress](https://www.cypress.io/) など
- テスト手法: ブラックボックステスト
- 責務: 主にSETエンジニア、次点でフロントエンドエンジニア
- 補足1: 一般的にシステム全体をテストするのがE2Eテストだと言われてますが、フロントエンドのテストコードなのでバックエンドはテストしません。
- 補足2: 文言が画面のどの位置に表示されたかはテストしません。 → しようと思えばできますが、コードがかなり複雑になるため不向きです。
## ビジュアルリグレッションテスト
![ヘッダーメニューが機能する_--_SERVICE_--_UIが表示されている.diff.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401234/40f0c6cd-2a05-e770-6862-099126ed6e3b.png)
- 別名: ビジュアルテスト、UIの見た目に着目したフロントエンドのテストコード
- 範囲: アプリケーションの視覚的な構造をテスト
- ツール: [Chromatic](https://www.chromatic.com/)、[Cypress](https://www.cypress.io/) など
- テスト手法: ブラックボックステスト
- 責務: 主にSETエンジニア、次点でフロントエンドエンジニア
- テストの方法: 事前にレイアウトが正しく表示されている画面(期待する結果)をスナップショットしておき、テスト自動化の最中に表示された画面(現状)との差分が、設定したしきい値を超えた場合にテスト失敗とする手法
- 補足1: OSまたはWebブラウザが異なる環境でテストすることにより失敗してしまうので、E2Eテストよりも失敗しやすいテストです。
## テストタイプの割合(テストピラミッド)
以下の画像は[事例で学ぶテストピラミッドを使ったテスト戦略](https://thinkit.co.jp/article/13346)から取得しました。
![テストピラミッド.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401234/310ee478-8cc4-5429-09f8-e86940eda710.png)
### アンチパターン
- ユニットテスト < APIテスト < E2Eテスト < ビジュアルテストの順でテスト範囲を広くすること → テストコードの作成と実行に時間がかかり非効率であるからです。
### ベストプラクティス
- ユニットテスト > APIテスト > E2Eテスト > ビジュアルテスト の順でテスト範囲を狭くすること → テストコードの作成と実行の速さ順に並んでいるため、それぞれのテストタイプの長所を活かしやすいからです。

# テスト自動化ツール
## 紹介
### [Selenium WebDriver](https://www.selenium.dev/ja/documentation/webdriver/)
- E2Eテスト自動化ツールの中で最も有名なツールです。
- AmazonではPythonでWebスクレイピングする書籍が多い印象があります。
- JS(JavaScript) / TS(TypeScript)以外の言語の使用が可能です。
- CI(継続的インテグレーション)でのテストファイルの並列処理が可能です。
- クロスブラウザテストに有効なツールです。
- ユニットテスト用のフレームワークの導入が必須です。
- 新しく開かれたタブの要素のテストが可能です。
- WebブラウザとWebDriverのバージョンを合わせる必要があります。
- 無料です。
### [Autify](https://autify.com/ja)
- ノーコードツールです。(別の言い方は、キャプチャー・リプレイツールです。)
- セルフヒーリングを搭載しています。(AIによるスタイルの自動修正のことです。)
### [Cypress](https://www.cypress.io/)
- Async / await構文が不要です。
- デフォルトで4秒間の待機が可能です。
- 公式ドキュメントにサンプルコードとどういう使い方が正しいのかが記載されています。
- JavaScriptインジェクション系に分類されます。
- 実現したいテスト自動化に対して少ないコードで実現可能です。
- 多くのテストタイプに対応しています。
- インストール時にテストフレームワークやアサーションライブラリなども付いてくるので、環境構築がとても簡単です。
- テストが失敗したときに、なぜ失敗したかを詳細に記述してくれるので修正がしやすいです。
- `cy.contains('apples')`と書くことで文字列が含まれている要素を取得することができるため、テストが壊れにくいコードで書くことが可能です。
- 実装方針としてページオブジェクトパターンが有名ですが、[Cypress公式サイトの記事](https://www.cypress.io/blog/2019/01/03/stop-using-page-objects-and-start-using-app-actions/)ではその方針を否定しています。 → テストコードは高度なプログラミング技法を使うよりも、パッと見でわかりやすいコードを書く方が良いということだそうです。(少し冗長な書き方になってもOKです。)
- mocha(テスティングフレームワーク)とchai(アサーションライブラリ)を採用しています。
- JS / TSのみに対応しています。
- それぞれのテストが終了すると、ローカルストレージとセッションストレージとクッキーをクリアします。
- 現時点でSafariのテストができません。
- 新しく開かれたタブの要素のテストができません。
- 無料で使う場合、テストファイルの並列処理が不可能です。
- 現時点で人気の[Jest](https://jestjs.io/ja/)が不採用です。

# E2Eテスト自動化の範囲
ベストプラクティスが明確に書かれている書籍やWebサイトがありませんでしたので、私なりに考えていることを以下に書きます。
- E2Eテスト自動化の範囲は30%までにした方が良いと書かれた書籍を見ましたので、それを参考に実装します。
- E2Eテストはエンドユーザ目線のテストなので、「エンドユーザが何を望んでサービスを受けるのか」を自動化するべきです。
- テストケースはユニットテストのような書き方であるため、それを基にE2Eテスト自動化を実装するべきではありません。例えば、ECサイトはエンドユーザが物を買うために利用するのであって、テストケースによくある「商品一覧画面から商品詳細画面へ遷移できること」をエンドユーザが主たる目的として望んで利用していないと思うからです。
- シナリオテストという「ユーザが一連の流れに沿ってシステムを問題なく利用できることを確認するためのテスト」の考え方を基に実装します。
- シナリオテストを作成してからテストコードを書くと、テスト範囲が明確になります。以下にシナリオテストで書くべきだと考えている項目を示します。
    - シナリオの番号
    - シナリオの大項目、中項目、小項目
    - 優先度
    - シナリオタイトル
    - シナリオ詳細
    - 項目
    - 前提条件
    - テスト手順
    - 期待値
- E2Eテスト実行中に各画面へ遷移したとき、またはモーダルが表示されたときにビジュアルリグレッションテストをすることで文言が正しいかやスタイル崩れが起きていないかなどを確認できるので、そのやり方が良いと思っています。

# E2Eテストのプログラミング技法
Cypressの[Best Practices](https://docs.cypress.io/guides/references/best-practices)の記事が他のE2Eテストフレームワークを選定していたとしても通じるプログラミング技法が書かれているので、おすすめです。
特に以下のベストプラクティスが参考になりました。
- E2Eテストしたい部分以外のところで、APIによるHTTPリクエストを送信することで実行速度を速くすること
- 要素の取得はなるべくクラス名を取得するのではなく、UIの文言を取得する方法で書くこと
- テストは常に互いに独立して実行でき 、かつパスできるようにすること
- E2Eテストでは、1つのテストに複数のアサーションを組み合わせること

# Cypressの実装例
以下のGIFは[アルサーガパートナーズ株式会社](https://www.arsaga.jp/)へE2Eテストを実行している様子です。
![cypress-arsaga-partners.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401234/73c2606b-d03f-a9ba-eefe-5cfde0cf8194.gif)
## ヘッドレスモードで動作させたときのテストの結果
Webブラウザをヘッドレスモードにするときは、ビジュアルリグレッションテストをしたいときか、CIでテストするときに用います。
### Cypressによるビジュアルリグレッションテスト
- [cypress-image-snapshot](https://github.com/jaredpalmer/cypress-image-snapshot)を使用しました。(スナップショットを撮る枚数の制限がなく、S3との連携が必須ではないことが利点です。)
- テストするときは期待する結果の画像と同じOSとWebブラウザを使用しなければなりません。
- 以下はテストが失敗したときのエラーメッセージです。
```
Error: Image was 0.15583590989399293% different from saved snapshot with 2258 different pixels.
See diff for details: /画像のパス/cypress/screenshots/画像のパス/画像名.png
```
- テスト失敗となったとき、左から順に期待する結果の画像、差分が出た部分を赤くハイライトした画像、現状の画像をまとめて1枚の画像として保存します。
- テストを実行するとき、ビジュアルリグレッションテストで失敗した画像をすべて削除してから実行する設定にしています。 → 残しておくと、どの時点のテストで失敗した画像なのかわからなくなるからです。
- 対象画面のスタイル崩れに対して1行のコードを書くだけでテストが可能です。 
```js
cy.matchImageSnapshot("ファイル名を記載");
```
## ファイルの説明
### test.cy.js(テストコード(概略))
テストコードは案件へ密接に関わるファイルですので、概略だけ紹介させていただきます。
- Cypressの言語仕様は、MochaのBDD(ビヘイビア駆動開発)がベースとなっています。
    - `describe()` … テストセットを宣言します。
    - `beforeEach()` … 各`it()`の実行時に1回だけ実行されます。
    - `it()` … 実行するテストコードを記述します。
```js:test.cy.js
describe("テストセット", () => {
  /* 省略 */
  beforeEach(() => { /* 省略 */ });
  it("実行するテストコード1", () => { /* 省略 */ });
  it("実行するテストコード2", () => { /* 省略 */ });
});
```
- Basic認証が設定されているWebサイトへアクセスするときは、各テストの実行前にBasic認証を成功させます。
```js:test.cy.js
beforeEach(() => {
  cy.visit("login", {
    auth: {
      username: Cypress.env("BASIC_AUTH_USERNAME"),
      password: Cypress.env("BASIC_AUTH_PASSWORD"),
    },
  });
});
```
### main.yml(GitHub Actionsを動作させるために必要)
- メインブランチへプッシュされたときと、注釈を外したときに毎日午前3時にGitHub Actionsを動作させるコードです。 → E2Eテスト時間がかかるので、深夜に自動でテストして出社時に結果を確認できるように工夫しました。
```yaml:main.yml
name: Cypress tests
on:
  push:
    branches:
      - main
  # schedule:
  # タイムゾーンはUTC(GMT) → 「日本の時刻から9時間マイナスした時刻」を指定
  # 分 時間 日間 Months 週の日数
  # - cron: "0 18 * * *" # 毎日午前3時(JST)に実行
```

- 実行環境のOSとブラウザを指定しているコードです → GitHubアカウントで無料版を使ってGitHubが用意している実行環境を使用したとき、GitHub Actionsは月2000分まで使うことができ、Linuxでは実行時間に1倍、Windowsでは実行時間に2倍、Macでは実行時間に10倍を乗算して消費するので、OSはLinuxを選択しました。
```yaml:main.yml
jobs:
  cypress-run:
    runs-on: ubuntu-latest # edgeを使用したいとき(消費2倍): windows-latest
    strategy:
      fail-fast: false # あるJobがFailしても、他のJobが途中で中断しないようにするため
      matrix:
        browser: [chrome] # ubuntuではEdge不可, ブラウザは同時に起動してテストするので、テストに依存関係があるときはブラウザは1つのみ指定すること
```
- 日本語が文字化けせず、正しく表示できるようにしています。
```yaml:main.yml
    steps:
      - name: Checkout
        uses: actions/checkout@v2 # Gitリポジトリの内容を取得
      - name: install japanese font
        run: sudo apt-get install -y fonts-noto-cjk # スクリーンショットの文字化けを防ぐため
```
- 環境変数をGitHubのActions secretsに隠しています。ここでは、Basic認証の情報を隠すと良いです。
```yaml:main.yml
      - name: Cypress run
        uses: cypress-io/github-action@v2
        env:
          CYPRESS_BASIC_AUTH_USERNAME: ${{ secrets.CYPRESS_BASIC_AUTH_USERNAME }}
          CYPRESS_BASIC_AUTH_PASSWORD: ${{ secrets.CYPRESS_BASIC_AUTH_PASSWORD }}
        with:
          command: yarn cy:run
          browser: ${{ matrix.browser }}
```
- テストが失敗したときだけ、テスト結果のレポートを生成します。
```yaml:main.yml
      - name: upload-mochawesome-report
        if: ${{ failure() }}
        uses: actions/upload-artifact@v2
        with:
          name: mochawesome-report (${{ matrix.browser }})
          path: |
            cypress/reports
```
- デバッグするときに実行中の様子を見たいので、テストが失敗したときだけ動画を撮影します。
```yaml:main.yml
      - name: upload-videos
        if: ${{ failure() }}
        uses: actions/upload-artifact@v2
        with:
          name: videos (${{ matrix.browser }})
          path: |
            cypress/videos
```
- テストが失敗したときだけ、Slackへ通知します。
```yaml:main.yml
      - name: Slack Notification on Failure
        if: ${{ failure() }}
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_USERNAME: CypressCLI
          SLACK_ICON: https://2.bp.blogspot.com/-H2eLSLfzvpA/XGjx1UapC6I/AAAAAAABRcA/5Xdh-W7tqk8X1YONndv2B1ykhJ6BRS1bgCLcBGAs/s800/ai_computer_sousa_robot.png
          SLACK_TITLE: Deploy Failure (${{ matrix.browser }})
          SLACK_COLOR: "#DB7177"
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_TEST_RESULTS }}
```
### cypress.config.js(Cypressの構成ファイル)
- `video: false`  … CIでテストが失敗したときに、その様子を見たいときはtrueへ変更します。
- `retries: { runMode: 0, openMode: 0,}`  … テストが失敗したときに再度同じところをテストする回数を指定するのですが、テストの前後に依存関係があるときに次のテストへ影響が出てしまうことがありましたので、再試行はしないことにしました。
- `screenshotOnRunFailure: false`  … 動画だとエラーメッセージが見えるので、失敗時のスクリーンショットはいらないと判断してfalseにしました。
- `baseUrl: "https://案件のURL/"`  … 案件のベースURLを指定します。
```js:cypress.config.js
const { defineConfig } = require("cypress");

module.exports = defineConfig({
  viewportWidth: 1280,
  viewportHeight: 800,
  video: false,
  defaultCommandTimeout: 4000,
  retries: {
    runMode: 0,
    openMode: 0,
  },
  screenshotOnRunFailure: false,
  reporter: "cypress-mochawesome-reporter",
  reporterOptions: {
    charts: true,
  },
  e2e: {
    setupNodeEvents(on, config) {
      // eslint-disable-next-line
      return require("./cypress/plugins/index.js")(on, config);
    },
    baseUrl: "https://案件のURL/",
  },
});
```
### cypress.env.json(環境変数を設定)
- ローカルリポジトリでは、実際の値を設定します。
```json:cypress.env.json
{
  "BASIC_AUTH_USERNAME": "???",
  "BASIC_AUTH_PASSWORD": "???"
}
```
### commands.js(カスタムコマンドを設定(抜粋))
- ヘッドレスモードかつUIのレンダリングが終わるであろう2秒を待機してからスナップショットを撮り、ビジュアルリグレッションテストができるようにしました。
```js:commands.js
Cypress.Commands.overwrite(
  "screenshot",
  (originalFn, subject, name, options) => {
    if (Cypress.browser.isHeadless) {
      cy.wait(2000).then(() => originalFn(subject, name, options));
    }

    return cy.log(`When "cypress open", no screenshot is taken.`);
  }
);
```
- 以下のコードでビジュアルリグレッションテストの細かい設定をします。
    - `capture: "fullPage"`  … ページ全体をキャプチャします。
    - `customDiffDir: "cypress/screenshots"`  … ビジュアルリグレッションが失敗したときの画像の保存場所を指定します。
    - `failureThreshold: 0.0033`  … 画像全体のしきい値を設定します。
    - `failureThresholdType: "percent"`  … しきい値の単位を設定します。
```js:commands.js
addMatchImageSnapshotCommand({
  capture: "fullPage",
  customDiffDir: "cypress/screenshots", // テスト実行前にテストが失敗した画像をCypressによって削除してもらうため
  failureThreshold: 0.0033, // %に0.01倍した値がしきい値
  failureThresholdType: "percent",
});
```
### package.json(scriptsを抜粋)
- スクリプトの意味は以下です。
    - `"cy:open"` … Webブラウザを立ち上げてテストするコマンドです。このとき、ビジュアルリグレッションテストはしません。
    - `"cy:run"` … Webブラウザをヘッドレスモードにしてテストするコマンドです。Google ChromeでE2Eテストとビジュアルリグレッションテストをします。
    - `"cy:run:snap:update"` … ヘッドレスモードでテストしつつ、ビジュアルリグレッションテストに使う期待する結果の画像を更新するコマンドです。
```json:package.json
  "scripts": {
    "cy:open": "cypress open --env failOnSnapshotDiff=false",
    "cy:run": "cypress run -b chrome",
    "cy:run:snap:update": "cypress run -b chrome --env updateSnapshots=true"
  }
```

# おわりに
## イソップ寓話(ぐうわ)の『アリとキリギリス』
以下の画像は[伊野孝行のイラスト芸術](http://www.inocchi.net/tv/aritokirigirisu/)から取得しました。
![aritokirigirisu6.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401234/46cc166f-2d64-00de-1c39-aa09c982c799.jpeg)
### あらすじ
夏の間にアリさんたちは冬の食料を蓄えるために働き続け、キリギリスさんはヴァイオリンを弾いて過ごしていました。
やがて冬が来てキリギリスさんは食べ物を探しますが見つからず、アリさんが夏に食料を集めていたことを思い出して分けてもらおうとアリさんの家を訪ねて食べ物を分け与えてもらいました。
めでたし。めでたし。
### 何がいいたいかといいますと...
テスト自動化を実装するかしないかについて、キリギリスさんのように短期的に得をして長期的に損する方を取るか、アリさんのように短期的に損をして長期的に得する方を取るか、あなたならどちらをベストプラクティスとしますか？

# 参考資料
https://qiita.com/jun2014/items/80e24a8e994c4bed6437

https://www.amazon.co.jp/%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%E3%83%86%E3%82%B9%E3%83%88%E8%87%AA%E5%8B%95%E5%8C%96%E3%81%AE%E6%95%99%E7%A7%91%E6%9B%B8-%E3%80%9C%E7%8F%BE%E5%A0%B4%E3%81%AE%E5%A4%B1%E6%95%97%E3%81%8B%E3%82%89%E5%AD%A6%E3%81%B6%E8%A8%AD%E8%A8%88%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9-%E6%9E%97-%E5%B0%9A%E5%B9%B3/dp/4297117363

https://postd.cc/frontend-testing-is-for-everyone/

https://www.oreilly.co.jp/books/9784873118161/

https://www.selenium.dev/ja/documentation/webdriver/

https://autify.com/ja

https://www.cypress.io/

https://docs.cypress.io/guides/references/best-practices