Vuetifyで作った画面のテストをCypressで自動化しようとした時に調べたことや、つまづいたこと

# はじめに

Vue.js、Vuetifyで作った画面をCypressでどうにか自動化したいと、いろいろ触って調査して気づいた点を書いていこうと思います。

# Cypressについて

Cypressは、E2Eテスティングフレームワークです。

[公式サイト](https://docs.cypress.io/guides/getting-started/installing-cypress.html#System-requirements)


# Cypress導入手順
## yarn install

```
yarn install cypress --save-dev
```

## testsフォルダにe2eフォルダを配置

`tests/e2e/fixtures`
`tests/e2e/plugins`
`tests/e2e/screenshots`
`tests/e2e/specs`
`tests/e2e/support`
`tests/e2e/videos`

### plugins/index.js

各フォルダ設定

```js
module.exports = (on, config) => {
  return Object.assign({}, config, {
    fixturesFolder: 'tests/e2e/fixtures',
    integrationFolder: 'tests/e2e/specs',
    screenshotsFolder: 'tests/e2e/screenshots',
    videosFolder: 'tests/e2e/videos',
    supportFile: 'tests/e2e/support/index.js'
  })
}

```

### support/index.js

Cypressのカスタムコマンドを追記するファイルの指定

```js

import './commands'

```

# サンプルテストコード

- `tests/e2e/spec/` フォルダ配下に、テストファイルを書く。
    - 表記ルールは特に無い。日本語でもOK（例：検索画面test.js）

```js

describe.skip('検索機能のテスト', () => {
  // テストケース(it関数）の直前で実行される
  beforeEach(() => {
    // Cypressコマンドのタイムアウト値を60秒に設定。（デフォルトは4秒）
    Cypress.config('defaultCommandTimeout', 60000);
    // 画面のサイズ指定
    cy.viewport(1940, 1080);
    // テスト開始時、どのURLにアクセスするかを指定
    cy.visit('http://localhost:8080');
    // ログインをするための、カスタムコマンド（後述）
    cy.login('user');
  });
  // テストケース(it関数）の直後で実行される
  afterEach( () => {
    cy.contains('ログアウト')
      .click();
  });

  // テストケース
  it('検索ができること', () => {
    // 目的の画面へ移動するためのカスタムコマンド（後述）
    cy.gotoTestPage();
    cy.wait(2000);
    cy.contains('検索')
      .click();

  });
});
```


# テスト実行

## 画面を見ながらのテスト

```shell

yarn run cypress open
```

以下のCypressの画面が開くので、テスト実行したいファイルをクリックします。

![image](https://user-images.githubusercontent.com/33717710/86101055-a61e1f80-baf4-11ea-8cdd-9fad113ae75e.png)

実行すると、このような画面が開く、テストコードの通りに実行が走ります。
![image](https://user-images.githubusercontent.com/33717710/86101156-d06fdd00-baf4-11ea-84b3-4f46030df522.png)

[Cypressの公式ページ](https://docs.cypress.io/guides/overview/why-cypress.html#Running-tests)
も参考にして下さい。

## コマンドライン上でのテスト

`headless` オプションを付けることにより、コマンドライン上で実行が可能です。

```shell script
yarn run cypress run --browser chrome --headless
```

![image](https://user-images.githubusercontent.com/33717710/86101486-4aa06180-baf5-11ea-94bd-627177d2e52b.png)

- テストが途中で実行終わると、失敗時のスクリーンショットが `e2e/screenshots/`に保存されます
- テスト実行の動作は、`e2e/videos/` にmp4が保存されます

# カスタムコマンド作成

- AppActionsという、Cypressに独自のコマンド（メソッド）を作成することができる機能。
- これにより以下の効果が期待できる
    - 内部の複雑なDOM操作などを抽象化して、テストコード自体を見やすくする
    - 特定のDOM操作を抽象化することで、idやinnerTextなどに変更があっても、カスタムコマンド内を修正するだけでよくなる（＝保守性向上）


[Cypress公式ページ](https://docs.cypress.io/api/cypress-api/custom-commands.html#Syntax)

以下の記事も参考にしてください

- [Page Object Patternを使うな、というCypress公式記事を読んで思ったこと \- Qiita](https://qiita.com/tumo-jp/items/541810ce77b1bdf56750)

## サンプルコード

`e2e/support/commands.js` に以下の様に記載することにより、
テストコード内で、`cy.login()` `cy.gotoTestPage()` と使用することができる。


```js

Cypress.Commands.add('login', username => {
  cy.get('[name=USER_ID]')
    .type(username);

  cy.get('[data-test=login]')
    .click();
});

Cypress.Commands.add('gotoTestPage',() => {
  cy.get('.navbar > div > .btn')
    .click();

  cy.contains('テストページ')
    .click();
});

```


# Cypressのテストコードでの注意点とかテクニック

## v-selectで`select()`が動作しない

type を使わないと駄目っぽい。

内部的にはselectタグでは無くて、別のもので動いているから？

[automated tests \- How to find element and select by cypress\.io with vue\.js v\-select? \- Stack Overflow](https://stackoverflow.com/questions/58298675/how-to-find-element-and-select-by-cypress-io-with-vue-js-v-select)


```html
  <v-select
    :data-test='test'
    :items="selectItems"
    item-text="name"
    item-value="val"
    dense
    outlined />
```

```js
cy.get('[data-test=test]').type('item01{enter}', {force: true})
```

ここの `{enter}` は特殊文字で、Enterを入力する操作扱いとなる。

参考：[type \| Cypress Documentation](https://docs.cypress.io/api/commands/type.html#Arguments)


## v-text-field で値を探す時

v-text-fieldは、HTMLにレンダリング後、labelとinputタグが並んで作られる

containsでlabelを探した後に、兄弟要素を探す next、prev でinputタグを取得できる


```js
  cy.contains('label', '取引先コード')
    .next('input')
    .type('001');
```

面倒だったら、`data-*` 属性を付与して get でもいける。

Cypress公式のベストプラクティスでは、`data-*` を付与してテストするほうが、HTML、CSSに依存しないので良いとしている。

[Best Practices \| Cypress Documentation](https://docs.cypress.io/guides/references/best-practices.html#Selecting-Elements)

ただし、要素内のテキストに変更があった場合にテストが失敗して欲しい時は、`cy.contains` を使うべきだとも言っている。

- 送信ボタン　→　保存ボタン　に変わった場合
    - 振る舞い自体が変わることになるので、テスト失敗して欲しい　→　`contains`
    - 失敗しないで欲しい　→　`cy.get([data-cy=hogehoge])`

このあたりはどうするかは、プロジェクトの方針にもよるかもしれません。

ちなみにまだ試してませんが、テストだけに使う `data-test` などの属性をプロダクション環境時に削除する方法はあるそう。

[vue\-loader 15で、テンプレート内の任意の属性\(data\-testなど\)を除外する \- Qiita](https://qiita.com/Sa2Knight/items/c2bbe199b1b1ff9336f5)



## v-checkbox でチェックを入れたり外したりする時は、force: trueオプション必要

check、uncheck を使えば良いと書いてあるが、普通にやってもうまくいかない。
forceオプションを入れると、うまくいく。

```js

cy.get('[data-test=hoge]')
  .check({force: true,});

cy.get('[data-test=hoge2]')
  .uncheck({force: true,});
}

```

## 途中でテストがこけると、それ以降のテストはスキップされる

- デフォルトの動作がそういうものらしい

[javascript \- Cypress: how to mark a single test as failed but continue to run other tests? \- Stack Overflow](https://stackoverflow.com/questions/57111538/cypress-how-to-mark-a-single-test-as-failed-but-continue-to-run-other-tests)


## タイムアウト値の設定を延ばす

Cypressのコマンドのタイムアウト値は、デフォルト4000msです。

SPAで取得したい要素がロードされる前にタイムアウトする場合は、個別にタイムアウト値を設定することができます。
 
```js
cy.get('button', { timeout: 30000 })
```

それすらも面倒だなと思った場合は、 `defaultCommandTimeout` を延ばすことができます。

```js
beforeEach(() => {
    Cypress.config('defaultCommandTimeout', 60000);
});
```

参考

- [Configuration \| Cypress Documentation](https://docs.cypress.io/guides/references/configuration.html#Timeouts)
- [javascript \- How to Wait until page is fully loaded \- Stack Overflow](https://stackoverflow.com/questions/56973019/how-to-wait-until-page-is-fully-loaded)

## ボタンのラベルの前後に空白が入っている場合の対処

これはCypressというより、Vue、Vuetifyの仕様の話だと思うけど、

ダイアログの登録ボタンをクリックしたいということで、
`cy.contains('登録').click()'` を書いたけど、何回も失敗してた。

consoleを確認すると、親ページの方で「登録確認」ボタンがあったので、そちらを取得してしまっていたのが原因だった。

containsは、指定テキストにマッチした一番最初の要素を取得するので、登録ボタンをピンポイントに取得する必要があったので、正規表現を使うことにした。


`cy.contains(/^登録$/).click()'`

だけど、これだと何も取得できなくてエラーとなる。

そんなわけないだろうと思って、検証ツール上で生成されたHTMLのtextを確認すると、


```

        登録
      
```

なぜか前後に余計な空白が…。

`cy.contains(/^ +登録 +$/).click()'`

そういうものだと諦め、前後スペース有りの正規表現でなんとか取得できた。

## テーブルに出てくる検索結果にアサーションを掛けたい場合

検索画面などで、検索ボタンを推してからテーブルに表示される結果にアサーションをしたい、なんてことがあると思います。

contains、parent、withinの組み合わせでなんとかできます。

取引先コード002で検索したら、検索結果にちゃんと出てきて、`テスト株式会社` という文字列が含まれているかどうかを確認したい場合、以下のようなコードでアサーションができます。

```js
// 取引先コード002の要素(td)の、親要素の行(tr)を取得し、withinでそのtr要素内でアサーションします。
cy.contains('002').parent('tr').within(()=> {
  cy.get('td').contains('テスト株式会社').should('be.visible');
});

```

`should('be.visible')` は取得した要素が表示されているかどうかをアサーションするコマンドです。

検索結果の任意の列の結果を見たいときもあると思います。
6列目の値が`2020-07-01`が入っていることを期待する場合は、以下のような感じになります。

```js
cy.contains('002').parent('tr').within(()=> {
  cy.get('td').eq(5).should('eq', '2020-07-01');
});

```

eqコマンドが、複数要素がある場合の何個目を取得する、といった形になります。

0から始まるので、配列とほぼ一緒と考えて良いと思います。


参考

- [withinを使ったテーブル内の値の取得](https://docs.cypress.io/api/commands/within.html#Tables)
- [should \| Cypress Documentation](https://docs.cypress.io/api/commands/should.html#Usage)
- [eq \| Cypress Documentation](https://docs.cypress.io/api/commands/eq.html)

余談ですが、単体のコマンドの説明だけではなく、こういったやりたいことが公式ドキュメントに整備されているのが、とても好感が持てます。



## うまく要素が取得できないな？　と思ったら

Cypressを使っていると、各コマンドが充実しており結構使いやすく感じます。

しかし、そもそもE2Eテストのコードを書いた経験が無いとか、HTMLとかにそこそこ詳しくないと、要素の取得とかで普通にハマります。（フロントエンド得意系の人だともっと早く解決できるのかもしれません）

そうなったときによくやるのが、`cypress open` でブラウザ表示した状態でテストし、Consoleで結果を確認していきます。


`cy.get('button')` と書いた場合、全てのテストが終わるか、もしくはコマンド終わった辺りでテストをStopし、ブラウザでコマンドが実行されたところをクリック

![image](https://user-images.githubusercontent.com/33717710/86193761-b8956900-bb87-11ea-8f55-3825ad4fb7a4.png)

検証ツールのConsoleから、どんな要素が取得できているかを確認できます。

![image](https://user-images.githubusercontent.com/33717710/86193769-c34ffe00-bb87-11ea-8dfa-dcfb227c83da.png)

そもそもこの時に何も取得できていなかったり、取得したい要素が無かったりしたら、指定が間違っている可能性があります。



# おわりに

癖みたいなものはありますけど、基本的に使い勝手は良いと感じています。
v-selectやv-text-fieldの要素取得のあれこれは、カスタムコマンドにまとめてしまえば他の人も扱いやすくなると思うので、本格的に導入を始めたらやっていければいいかなと思っています。

# 参考記事

- [Cypressで送る快適E2Eライフ \- Qiita](https://qiita.com/aomoriringo/items/187af32eeac869182648)
- [Cypress \- TypeScript Deep Dive 日本語版](https://typescript-jp.gitbook.io/deep-dive/intro-1/cypress)
- [Cypressで始めるReactのE2Eテスト\-導入から実際にテストを書いてみよう！](https://microcms.io/blog/cypress-react-e2e/)
- [打鍵テストをCIで回す：Cypress on GitHub Actions \| フューチャー技術ブログ](https://future-architect.github.io/articles/20200115/)
- [Cypressの拡張メソッドを使ってテストコードを判り易く！ \| cloud\.config Tech Blog](https://tech-blog.cloud-config.jp/2019-07-01-cypress-extension-method/)