はじめましてCypress

業務でcypressを導入する機会があったので、よく使用したものや自分がつまったところなどを忘れないよう備忘録としてまとめました。
最後には最終的な書いたコードも載せておきます。cypressというかe2eテスト自体が初めてなのでこれで合っているのか&冗長なところがあったりするかもしれませんが、少しでも誰かの参考になればcyわいです。
何か間違っているところやもっといい書き方あるよ！って方いたら教えていただけますとcyわいです。

## Install
npm

```
npm install cypress --save-dev
```

yarn

```
yarn add cypress --dev
```

### cypressの起動コマンド

**GUIでの実行する場合**

npxとはnpmパッケージを簡単に実行できるコマンド。npm@5.2.0から同梱されている。
下記コマンドを実行すると、cypressディレクトリが生成され中にサンプルのテストファイルなどが作られる。

```
$ npx cypress open
```
作成されるファイルは以下の通り

```
cypress
├── fixtures
│   └── example.json
├── integration
│   └── examples // 下記サンプルは必要なければ削除してよい
│       ├── actions.spec.js
│       ├── aliasing.spec.js
│       ├── assertions.spec.js
│       ├── connectors.spec.js
│       ├── cookies.spec.js
│       ├── cypress_api.spec.js
│       ├── files.spec.js
│       ├── local_storage.spec.js
│       ├── location.spec.js
│       ├── misc.spec.js
│       ├── navigation.spec.js
│       ├── network_requests.spec.js
│       ├── querying.spec.js
│       ├── spies_stubs_clocks.spec.js
│       ├── traversal.spec.js
│       ├── utilities.spec.js
│       ├── viewport.spec.js
│       ├── waiting.spec.js
│       └── window.spec.js
├── plugins
│   └── index.js
└── support
    ├── commands.js
    └── index.js
```
cypressのGUI画面がでてくるので、テストを実行したいファイルをクリックするだけ。また、該当のテストを修正し、保存するたびにテストを再度実行してくれる。

**CLI（ヘッドレス）で実行する場合**

```
$ npx cypress run -s cypress/integration/***.spec.js
```

## 各ディレクトリについて
### fixtures
テストで使用できる静的データの外部ピースとして使用する。

```json:cypress/fixtures/example.json
{
  "name": "はんま",
  "email": "baki@gmail.com",
  "body": "地上最強の高校生"
}
```
```js
cy.fixture('example.json') // fixtures/example.jsonからデータを取得
```

### integration
ここにテストファイルを作成する。

### plugins
Develop環境やStaging環境など、環境変数などを動的に変更するときなどに使用する。

**参考資料**
[Configuration API](https://docs.cypress.io/api/plugins/configuration-api.html#Usage)
[Cypressを使ってUIテストを作成してみた](https://qiita.com/o_sol06/items/61c589ad7300d532a5b7#develop%E7%92%B0%E5%A2%83%E3%81%A8staging%E7%92%B0%E5%A2%83%E3%81%AE%E5%88%87%E3%82%8A%E6%9B%BF%E3%81%88)
### support
カスタムコマンドを作成できる。また既存のコマンドを上書きすることも可能。
`cypress/support/index.js`で、importを介してテストファイルが評価される前にロードされるため、コマンドを定義または上書きは `cypress/support/commands.js` に記述すると良い。

```js:support/command.js
Cypress.Commands.add('inputCheck', ({ name, email, age }) => {
  cy.get('#name').type(name).should('have.value', name)
  cy.get('[type="email"]').type(email).should('have.value', email)
  cy.get('[type="radio"]').check(age, { force: true })
});
```
```js:integration/example.spec.js
cy.inputCheck({
  name: 'はんま',
  email: 'baki@gmail.com',
  age: '16'
});
```

### screenshots
スクリーンショットしたファイルが作成される。コマンド`cy.screenshot()`でスクショをとることも可能。
videoを録りたくない場合は、cypress.jsonに下記追加する。

```json:cypress.json
{
  "video": false
}
```


## コマンド
基本的にAssertionsやCommandsは[公式](https://docs.cypress.io/api/api/table-of-contents.html)を参考にすると良い。

### get
DOMの取得

```js
// 指定のDOMを取得
cy.get('#name')
  // 上記で取得したフィールドにはんまを入力
  .type('はんま')
  // はんまが入力されているか
  .should('have.value', 'はんま')
```

### contains
指定の文字列が含まれている要素を取得

```js
cy.contains('確認').click()
```

### check
チェックボックスまたはラジオボタンをチェック。
`cy.check() failed because this element is currently disabled`のエラーが出るとき。
ラジオボタンやセレクトボックスのテスト時、{force: true}オプションを許可して選択オプション等のエラーチェックを無効にする。

**参考資料**
https://github.com/cypress-io/cypress/issues/107

```js
cy.get('[type="radio"]').check('東京', { force: true })
```

### only
.only（）を追加することで、指定したスイートまたはテストケースのみを実行できる。

```js
describe('Array', function() {
  describe.only('#indexOf()', function() {
    // ...
  });
});
```

### skip
.only（）の逆。 .skip（）を追加することで、テストケースを単に無視するように指示できる。

```js
// スイート全体（describe）にも付与することも可能
describe('Array', function() {
  describe('#indexOf()', function() {
    it.skip('should return -1 unless present', function() {
      // this test will not be run
    });

    it('should return the index when present', function() {
      // this test will be run
    });
  });
});
```

### debug
**デバッグしたいとき**
検証したいところに.debug()を付与。コンソールで内容を確認できる。（chromeで検証するときと同じようにcypressGUI側画面でコンソールを開く）

```js
cy.get('#name')
  .type('はんま')
  .debug()
```

## 個人的に詰まったところ
### テストが成功する時と失敗する時がある
原因は、sessionStorageで前の入力内容が残っていることが問題だった。ちなみに、localstrageとcookieの内容はテスト実行時毎回クリアしてくれているとのこと。

**参考資料**

https://github.com/cypress-io/cypress/issues/413
### 対応策
下記のように、sessionStorage.clear()でクリアすること。

```js
beforeEach(() => {
  cy.window().then((win) => {
    win.sessionStorage.clear()
  })
})
```
cy.visitの中で、onBeforeLoadオプションを利用してページがすべてのリソースをロードする前に呼び出すことも可能。

```js
beforeEach(() => {
  cy.visit('http://localhost:3001/apply', {
    onBeforeLoad: (win) => {
      win.sessionStorage.clear()
    }
  })
})
```

### target="_blank"のテスト
**参考資料**
[Cypress で特定のページにナビゲートされたか確かめるには](https://qiita.com/karszawa/items/ce89661394c65d429fef)
https://stackoverflow.com/questions/48348354/cypress-get-href-attribute

```js
cy.contains('プライバシーポリシー').should('have.attr', 'href', '/privacy').invoke('removeAttr', 'target').click()
cy.url().should('include', '/privacy')
```

### ファイルのテスト
プラグインの[cypress-file-upload](https://www.npmjs.com/package/cypress-file-upload)を導入

```js
const file = 'files/cypress_test.xlsx'
cy.get('[type="file"]').attachFile(file)
```

## 最終的なテストコード

```js:integration/apply/index_spec.js
describe('apply', () => {
  beforeEach(() => {
    cy.window().then((win) => {
      win.sessionStorage.clear()
    })
  })
  const baseURL = Cypress.env('baseURL')
  const name = 'はんま'
  const email = 'baki@gmail.com'
  const age = '16'
  const file = 'files/cypress_test.xlsx'

  describe('正常系のテスト', () => {
    it('入力項目を入力後、確認ボタンを押して確認画面へ遷移すること', () => {
      cy.visit(baseURL + 'apply')
      // 確認ボタンが無効状態
      cy.contains('確認').should('be.disabled')
      // 必須項目入力
      cy.inputCheck({
        name: name,
        email: email,
        age: age
      });
      cy.get('[type="tel"]').type('03011112222').should('have.value', '03011112222')
      cy.get('[type="file"]').attachFile(file);

      // 確認ボタンが有効状態
      cy.contains('確認').should('not.be.disabled').click()
    })

    it('確認画面で入力内容が反映されていること', () => {
      // 確認画面
      cy.get('.confirm').should('not.have.css', 'display', 'none')
      // 入力内容確認
      cy.get('.confirm').contains('入力内容').parent('dl').within(() => {
        cy.get('dd').eq(0).should('contain', 'はんま')
        cy.get('dd').eq(1).should('contain', 'baki@gmail.com')
        cy.get('dd').eq(2).should('contain', '16')
        cy.get('dd').eq(3).should('contain', '03011112222')
        cy.get('dd').eq(4).should('contain', 'cypress_test.xlsx')
      })
    })

    it('内容修正ボタンで入力画面に戻れること', () => {
      cy.contains('内容を修正する').click()
      // 入力画面に戻る
      cy.get('.confirm').should('have.css', 'display', 'none')
    })

    it('送信を押してサンクス画面に遷移すること', () => {
      // 確認ボタンを押して確認画面へ
      cy.contains('確認').should('not.be.disabled').click()
      cy.get('button').contains('送信').click()
      cy.url().should('include', '/apply/thanks')
    })
  })

  describe('エラーテスト', () => {
    it('入力が間違っていたらエラーメッセージが表示されること', () => {
      cy.visit(baseURL + 'apply')
      // エラーメッセージがある項目をそれぞれ入力後クリアしエラーを表示させる
      cy.get('#name').type(name)
      cy.get('#name').clear()
      cy.get('[type="email"]').type(email)
      cy.get('[type="email"]').clear()

      // エラーメッセージが表示されているか確認
      cy.contains('入力内容').parent('dl').within(() => {
        cy.get('dd').eq(1).should('contain', '氏は必須項目です').and('contain', '名は必須項目です')
        cy.get('dd').eq(2).should('contain', '氏（かな）は必須項目です').and('contain', '名（かな）は必須項目です')
        cy.get('dd').eq(3).should('contain', 'メールアドレスは有効なメールアドレスではありません')
      })
    })

    it('ファイルのアップロードが有効でない拡張子の場合エラーメッセージが表示されること', () => {
      const error_file = 'files/test-image.png'
      cy.get('[type="file"]').attachFile(error_file)
      cy.contains('入力内容').parent('dl').within(() => {
        cy.get('dd').eq(6).should('contain', 'スキルシートは有効なファイル形式ではありません')
      })
    })
  })

  describe('別タブ（target="_blank"）のテスト', () => {
    it('プライバシーポリシーのリンクが確認できること', () => {
      cy.visit(baseURL + 'apply')
      cy.contains('確認').should('be.disabled')
      cy.inputCheck({
        name: name,
        email: email,
        age: age
      });
      cy.contains('確認').should('not.be.disabled').click()
      // 確認画面
      cy.contains('プライバシーポリシー').should('have.attr', 'href', '/privacy').invoke('removeAttr', 'target').click()
      cy.url().should('include', '/privacy')
    })
  })
})
```



## 参考記事
[Cypressを使ったインテグレーションテストの導入](https://qiita.com/sinpaout/items/4313ed75e603508696b1#cypress%E3%81%AE%E5%B0%8E%E5%85%A5)
[E2Eテストフレームワーク Cypress の運用Tips](https://qiita.com/yousan/items/3c734852292b97f26e77)
[Cypress 導入の方法まとめ](http://oisham.hatenablog.com/entry/2019/07/20/193902)
[Cypressの記法メモ](https://yaruki-strong-zero.hatenablog.jp/entry/cypress)
[Cypressテストコードまとめ](https://qiita.com/ryuseiyarou/items/459672caf7978b788c0f)