Cypress で html テストレポート (mochawesome) を出力する

# Cypress で html テストレポート (mochawesome) を出力する

[Cypress](https://www.cypress.io/) には [Cypress Dashboard](https://www.cypress.io/dashboard/) というテスト実行と実行結果のレポートを生成する素晴らしいサービスがありますが、これを諸事情により使えないけど html テストレポートが欲しいという場合に、自前で生成する方法を説明します。 Dashboard 完全互換ではありません。

## 下準備

単純な状態を作るために Cypress だけを依存関係に追加したパッケージを作り、初期化します。

```sh
yarn init
yarn add --dev cypress
yarn cypress open
```

この記事では `yarn` を使います。 `npm` を利用される方は適宜読み替えてください。

これにより Cypress の設定ファイルである `cypress.json` が直下に、テストファイル等が `cypress` ディレクトリ以下に作成されます。

## html レポートを出力する

### mochawesome をレポーターとして設定する

Cypress は Mocha ベースで作られているため Mocha に対応したレポーターを利用することができます。
今回は [mochawesome](https://adamgruber.github.io/mochawesome/) という html レポーターを利用します。

まずは mochawesome を依存関係に追加しますが、レポーターに `peerDependencies` が指定されている場合、それも追加する必要があります。
mochawesome には `mocha` が peerDependencies として指定されているため、 Mocha も追加します。
なお、本記事執筆時点での最新版の Mocha は 6 系ですが、使用すると `Cannot read property 'passes' of undefined` というエラーが出てしまうため、 5 系 に落とす必要があります。[^1]

```sh
yarn add --dev mochawesome mocha@^5.2.0
```

次に Cypress に mochawesome をレポーターとして設定します。
`cypress.json` に追記します。

```cypress.json
{
  "reporter": "mochawesome"
}
```

この状態でテストを実行してみます。

```sh
yarn cypress run
```

テストが完了すると直下に `mochawesome-report` というディレクトリができています。
これが、 mochawesome により作成された html レポートになります。
設定により出力ディレクトリを変更することが可能[^2]ですが、今回は出力先を変更しません。
中身を見ると `mochawesome.html` と `mochawesome.json` と `assets` というディレクトリができています。[^3]

`mochawesome.html` というのが出力された html レポートなのでこれを確認してみると、いくつかあるスペックファイルのうちの一つのスペックの結果しか出力されていません。
Cypress はスペック毎に出力を行い、出力のたびにレポートを **上書き** するため[^4]で、上書きしないようにする必要があります。

なお、 Cypress がレポートを出力する際、既存のレポートを消すことはないので、テスト実行前に自前でレポートを必ず消す必要があります。

### スペック毎にレポートを出力する

スペック毎にレポートを出力するには cypress.json で設定をします。

```cypress.json
{
  "reporter": "mochawesome",
  "reporterOptions": {
    "overwrite": false
  }
}
```

この状態で再度テストを実行してみると、 mochawesome-report 以下に連番の html ファイルと json ファイルが作成されます。
中身を確認するとスペック毎に結果が出力されていますが、これだとスペックファイルが増えるたびに参照先が増えることになってしまうので、一つのファイルにまとめるようにします。

### mochawesome のレポートをマージする

#### 中間ファイルをマージする

mochawesome のレポートを一つにまとめるには [mochawesome-merge](https://github.com/Antontelesh/mochawesome-merge) というツールを使います。
まずは、依存関係に追加します。

```sh
yarn add --dev mochawesome-merge
```

そして、 cypress.json の設定を変更します。
mochawesome-merge で中間ファイルの json を一つにまとめた後に、別途 html レポートを生成するため、 html の出力を無効にし、 json だけを出力するように変えます。

```cypress.json
{
  "reporter": "mochawesome",
  "reporterOptions": {
    "overwrite": false,
    "html": false,
    "json": true
  }
}
```

この状態でテストを実行すると mochawesome-report 下に json がいくつか作成されます。
これを mochawesome-merge で一つの json にまとめます。
まとめる方法として、 js のスクリプトを書く方法とコマンドラインで実行する方法がありますが、ここではコマンドラインで実行する方法を利用します。[^5]

```sh
yarn --silent mochawesome-merge > mochawesome.json
```

ここでの注意点として `--silent` オプションを yarn に渡すのを忘れないようにしてください。
渡さないと yarn 自体の出力が json に書き込まれてしまい、 json を読み込もうとした時に文法エラーになってしまいます。
npm scripts の場合には `mochawesome-merge > mochawesome.json` で問題ありません。

このコマンドを実行すると mochawesome-report 以下の json に出力された結果が、直下の `mochawesome.json` という一つの json にマージされます。
出力先を変更する場合にはリダイレクト先を変更します。
また、レポート出力先ディレクトリを変更している場合には、 `--reportDir` という引数にそのディレクトリのパスを渡します。

#### マージされた中間ファイルから html レポートを出力する

中間ファイルからレポートを出力します。レポートを出力するのに [mochawesome-report-generator(marge)](https://github.com/adamgruber/mochawesome-report-generator) を利用します。
marge は mochawesome-report が依存しているため依存関係に追加せずに利用できます。

```sh
yarn marge mochawesome.json
```

引数に html レポートの元となる json ファイルを指定して実行します。
実行すると mochawesome-report 下に `mochawesome.html` と `assets` ディレクトリができ、 mochawesome.html を参照するとすべてのスペックがマージされた結果が出力されていることが確認できます。

#### 出力先の変更

mochawesome-report 下の assets と mochawesome.html をホストすれば結果を公開できるのですが、 mochawesome-report 下には中間ファイルのスペック毎の json があり邪魔です。
邪魔なファイルを消すという手もありますが、 marge には出力先を変更するオプション `-o` がある[^6]ので、これを利用して mochawesome-report ではないところに出力するようにします。
この後に利用するスクリーンショットや動画の添付の際に相対パスを利用するため cypress 下にレポートを出力するようにします。

```sh
mkdir cypress/report
yarn --silent mochawesome-merge > cypress/report/mochawesome.json
yarn marge cypress/report/mochawesome.json -o cypress/report
```

## スクリーンショットや動画をレポートに添付する

Cypress は `cypress run` での実行時に、テスト失敗時のスクリーンショットおよびテスト結果に関わらずテスト開始から終了までの動画を自動で cypress ディレクトリ以下に保存します。[^7]
しかし、単にレポート出力を行っただけだとレポートには添付されないため、レポートに添付できるようにします。

### mochawesome のテスト結果に情報を追加する

mochawesome には各テスト結果に情報を追加する `addContext` というメソッドがあります。
このメソッドは様々な種類の情報を添付することができる[^8]ので、このメソッドを利用してスクリーンショットおよび動画をテスト結果に添付します。

### Cypress で mochawesome の addContext を呼び出す

#### イベントで addContext を呼び出す

Cypress では Cypress の機能を拡張するプラグインの仕組み[^9]が用意されていますが、今回の目的に適した拡張できるイベントがありません。
`after:screenshot` というイベントがありますが、スクリーンショットが取得された時だけで動画には対応できませんし、 addContext に渡すテストオブジェクトが取得できません。

そこで今回はデバッグ目的[^10]とされているイベントフックを利用します。
イベントは `cypress/support/index.js` から以下のようにフックします。[^11]

```cypress/support/index.js
Cypress.on(イベント名, コールバック);
```

利用できるイベントの中にテスト実行後に呼び出される `test:after:run` というのがあり、
このイベントはテストの各種属性と Mocha の runnable を引数として受け取ります。

このコールバックの中で先ほどの addContext を呼び出します。
スクリーンショットは失敗時のみ取得されるためテスト結果を判定する必要があります。
テスト結果はコールバックの第一引数のオブジェクトの `state` プロパティが `failed` かどうかで判定できます。
addContext の第一引数のテストオブジェクトの `test` というプロパティにコールバックの第一引数をセットしたオブジェクトを渡します。
addContext の第二引数にはスクリーンショットへのパスと動画へのパスを渡します。

```cypress/support/index.js
import addContext from 'mochawesome/addContext';

Cypress.on('test:after:run', (test, runnable) => {
  if (test.state === 'failed') {
    addContext({ test }, 'スクリーンショットへのパス');
  }
  addContext({ test }, '動画へのパス');
});
```

#### スクリーンショットおよび動画のパスの取得

スクリーンショットが保存されるパスはデフォルトでは以下のようになっています。[^12]

`{screenshotsFolder}/{specPath}/{testName} (failed).png`

各値は以下のように取得できます。

- screenshotsFolder: スクリーンショットが保存されるベースとなるディレクトリ。設定により変更可能 [^13] で、デフォルトでは `cypress/screenshots`。 `Cypress.config('screenshotsFolder')` で取得可能だが、絶対パスになるため相対パス化が必要。今回の例ではデフォルト値を利用し起点となるのが cypress/report なので、 `../screenshots` になる。
- specPath: スペックファイルのファイル名。 `location.pathname` からディレクトリ付きで取得可能だが、 `/__cypress/iframes/integration/ディレクトリ/ファイル` というような形になるので変換には若干工夫が必要。
- testName: コールバックの第二引数のプロパティ `parent.title` および `title` から取得可能。

動画が保存されるパスは以下のようになっています。

`{videosFolder}/{specPath}.mp4`

こちらも各値は以下のようになっています。

- videosFolder: スクリーンショットが保存されるベースとなるディレクトリ。設定により変更可能 [^13] 。 `Cypress.config('videosFolder')` で取得可能だが、絶対パスになるため相対パス化が必要。
- specPath: スペックファイルのファイル名。 `location.pathname` からディレクトリ付きで取得可能だが、 `/__cypress/iframes/integration/ディレクトリ/ファイル` というような形になるので変換には若干工夫が必要。

上記のようなことから cypress/support/index.js の中は最終的に以下のようになります。

```cypress/support/index.js
import addContext from 'mochawesome/addContext';

Cypress.on('test:after:run', (test, runnable) => {
  if (test.state === 'failed') {
    addContext({ test }, `../screenshots/examples/${location.pathname.replace(/(.*)\//, '')}/${runnable.parent.title} -- ${test.title} (failed).png`);
  }
  addContext({ test }, `../videos/examples/${location.pathname.replace(/(.*)\//, '')}.mp4`);
});
```

##### 注意点

- 上記のパス指定の仕方だとテストのネストの仕方によっては上手く行かないかもしれません。
- スペックのディレクトリおよびファイル名の取得は手抜きです。
- 上記はエラー時に自動で取得されるスクリーンショットだけを考慮しており、自前でスクリーンショットを取得する場合[^14]は考慮していません。自前でスクリーンショットを取得したものをレポートに追加するのは、フックできるのが after:screenshot しかなく、前述したように test オブジェクトを取得することができなさそうなので、無理なように思えます。

## 実行コマンド

最終的な実行コマンドは以下のようになります。

```sh
rm -rf mochawesome-report cypress/report
yarn cypress run
mkdir cypress/report
yarn --silent mochawesome-merge > cypress/report/mochawesome.json
yarn marge cypress/report/mochawesome.json -o cypress/report
```

これであとは以下のディレクトリをホストすればテスト結果をスクリーンショットおよび動画付きで公開することができます。

- cypress/report
- cypress/screenshots
- cypress/videos

## サンプル

ここまでをまとめたサンプルは以下のリポジトリにおいてあります。

https://github.com/kobanyan/cypress-mochawesome-example



[^1]: https://github.com/cypress-io/cypress/issues/3537 を参照。
[^2]: cypress.json の `reporterOptions.reportDir` で設定。
[^3]: 詳細は https://github.com/adamgruber/mochawesome#output を参照。
[^4]: https://docs.cypress.io/guides/tooling/reporters.html#Report-per-spec を参照。
[^5]: スクリプトで実行する方法は https://github.com/Antontelesh/mochawesome-merge#cypress を参照。
[^6]: その他のオプションは https://github.com/adamgruber/mochawesome-report-generator#options を参照。
[^7]: 詳細は https://docs.cypress.io/guides/guides/screenshots-and-videos.html を参照。オプションにより挙動を変えることができます。
[^8]: https://github.com/adamgruber/mochawesome#adding-test-context を参照。
[^9]: https://docs.cypress.io/api/plugins/writing-a-plugin.html を参照。
[^10]: https://docs.cypress.io/api/events/catalog-of-events.html を参照。
[^11]: 設定により変更可能。 https://docs.cypress.io/guides/references/configuration.html#Folders-Files を参照。
[^12]: https://docs.cypress.io/api/commands/screenshot.html#Naming-conventions を参照。
[^13]: https://docs.cypress.io/guides/references/configuration.html#Screenshots を参照。
[^14]: https://docs.cypress.io/api/commands/screenshot.html