Next.js × CypressでE2Eテストをする

Next.jsをベースにブログサイトを作って、そのブログサイトのE2EテストをCypressで実装してみました。

## 対象読者

- E2Eテストの書き方がわからない
- Cypressを使ってみたい
- なんとかログイン導線は死守したいけど、そもそもE2Eテスト環境をまだ導入していない

## E2Eテストとは
E2EテストはEnd to End Testの略です。Unitテストのように１つの関数に対してテストをするのではなく、システム全体に対してテストを行います。

例えば、メールアドレスとパスワードを入れてログインするページがあったとします。
Unitテストでは、メールアドレスやパスワードを入れたときにそれぞれの書式が正しいかどうか判定するための関数をチェックします。
E2Eでは、メールアドレスとパスワードを入れてログインボタンを押し、正しくログインしたあとのページに遷移しているかをチェックします。​
​
## Cypress

E2EテストをリッチなUIでよしなに実行できます。
実際に使ってみて思ったこととしては、テストがなぜこけたのかがスナップショットからわかることで、修正のスピードが上がりました。また、仕様と違う動作をしているかどうかをすぐ確認できたり、Cypressの標準機能でiPhone8にサイズを変えたり、MacBookのサイズに変えたりしたことでUIが崩れてボタンが押せなくなったなどの人間が見たら一発でわかるような仕様にも気付けました。Real time reloads機能があることでテストを書き直したりコンポーネントをいじったあと、目視ですぐに確認できる体験もよかったです。
何かとテストを書くことが置き去りになってしまいますがデバッグの質が上がるだけでなく、テストを書く体験もよくなるので積極的に導入検討していただけたらと思います。詳しくはこちらの動画をみていただけたらと思います。[Cypress Vimeo](https://vimeo.com/237527670)

## Cypressを体験してみる
### 準備
`$ git clone https://github.com/wataryooou/nextjs-cypress.git`​
`$ cd nextjs-cypress`
`$ yarn`
`$ yarn dev`
`http://localhost:3000/`にアクセスして、無事に起動したか確認してください。
​

### 作ったもの
こんな感じのブログをテストします。[こちら](https://github.com/vercel/next-learn-starter/tree/master/demo)を参考に実装しました。コードは[ここ](https://github.com/wataryooou/nextjs-cypress)に貼っておきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104635/129a2c2d-31d3-8401-e0e0-5f568d9a7fb0.png" width="680px">


### Cypressの実行
`$ yarn cy:open` をすると以下のようなGUIが表示されます。このGUIの `index.spec.js` をクリックするとそのテストが走ります。右側にある `Run 6 integration specs` をクリックすることで全てのテストを走らせることも可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104635/22d63e25-307a-ce3b-34fd-1da549494add.png)

`$ yarn cy:run` では全てのテストをCLIで実行できます。
`$ yarn cy:run --spec <file path>` ではfile pathのテストだけCLIで実行できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104635/17cc6704-69dc-0ae7-8198-39f472c3ce19.png)

### 各テストの書き方
#### 特定の画面が表示されているか
画面が表示されていることを満たす条件を考えましょう。今回の場合だと、
1. 指定したURLにアクセスしている
2. そのページに必ずあるものが表示されている

この２つを担保するテストが通れば、そのページが表示できているとします。では、その２つのテストを上に貼ってある「Webcome to page!」が表示されたページのテストを書いていきます。

1に関しては`cy.location("pathname", { timeout: 10000 }).should("include", "/");`を使うことでURLを取得できます。このページのURLは`http://localhost:3000/` なので、上のままで大丈夫です。

2に関しては「Webcome to page!」が表示されているはずなので、この文言が正しく表示されているか確認します。その場合は２つあって、１つは`cy.get("p").contains("Welcome to page!");`というように `pタグ` に文言が入っているか調べるやり方です。
もう１つはhtmlに`<p data-cy="welcome">Welcome to page!</p>`というように、データタグを付与するやり方です。基本的にはこちらを推奨しています。この場合、cypressのテストコードでは`cy.get("[data-cy=welcome]").contains("Welcome to page!");`と書いてあげると、タグの文言に正しく「Welcome to page!」が入っているかをチェックできます。

後者をおすすめする理由はたくさんありますが、htmlをpタグからspanタグに入れ替えたときにテストコードも変える手間がなくなったり、複数ボタンが存在するときに特定のボタンをクリックできたりとテストの安全性が高まります。

<details><summary>サンプルコード: 特定の画面が表示されているか</summary><div>

```js:index.spec.js
/// <reference types="cypress" />

context("/ - Home", () => {
  beforeEach(() => {
    cy.visit("/");
  });

  it("アクセスしたURLがあっている", () => {
    cy.location("pathname", { timeout: 10000 }).should("include", "/");
  });

  it("ページが表示されている", () => {
    cy.get("[data-cy=welcome]").contains("Welcome to page!");
  });
});
```

`beforeEach`はそれぞれのテスト(it)前に実行するものを書きます。この場合、`cy.visit("/");`が書いてありますがこれは`/` ページにアクセスするという意味です。

このサンプルコードの実行順序は、
1. `/` ページにアクセスする
2. アクセスしたURLがあっている - test
3. `/` ページにアクセスする
4. ページが表示されている - test

となっています。E2EテストはそこまでUnitテストのように分割する必要がないのと、これだと毎回アクセスするためより多くの時間がかかってしまいます。

そのため、次の改良版で書いているように、２つのテストをまとめてしまうことが望ましいです。この変更でどれくらい早くなるかというと、改良前は **1.47sec** かかっていたものが改良後には **0.75sec** になりました。

</div></details>

<details><summary>改良版サンプルコード: 特定の画面が表示されているか</summary><div>

```js:index.spec.js
/// <reference types="cypress" />

context("/ - Home", () => {
  beforeEach(() => {
    cy.visit("/");
  });

  it("ページが表示されている", () => {
    cy.location("pathname", { timeout: 10000 }).should("include", "/");
    cy.get("[data-cy=welcome]").contains("Welcome to page!");
  });
});
```
</div></details>


#### ユーザの操作に対して正しい挙動をするかチェックする

ユーザが操作することといえば、何かをタイピングしたり、ボタンをクリックしたりすることが挙げられます。
今回はこの２つをテストでチェックしていきたいと思います。

##### タイピング
`cy.get("[data-cy=input]").type("タイプしたい文字列");`とすることで、その要素に対してタイピングすることができます。
/sampleページでタイプしたものがそのまま表示されるかテストしているのでそちらを参考にしてみてください。

<details><summary>サンプルコード: タイピングしたら同じものが表示される</summary><div>

```js:index.spec.js
/// <reference types="cypress" />

context("/sample - サンプルページ", () => {
  beforeEach(() => {
    cy.visit("/sample");
  });

  it("タイピングしたら同じものが表示される", () => {
    cy.get("[data-cy=input]").type("123456abcdef");
    cy.get("[data-cy=output]").contains("123456abcdef");
  });

});

```
</div></details>

##### 要素のクリック

<details><summary>サンプルコード: 全てのページに遷移できるか</summary><div>
`cy.get("[data-cy='About']").click();`とすることで、その要素に対してクリックすることができます。
ページ上部に表示されているnavをクリックして各ページに遷移しているかをテストするコードを書いたのでそちらを参考にしてみてください。

```js:index.spec.js
/// <reference types="cypress" />

context("/ - Home", () => {
  beforeEach(() => {
    cy.visit("/");
  });

  it("全てのページに遷移できる", () => {
    cy.get("[data-cy='About']").click();
    cy.location("pathname", { timeout: 10000 }).should("include", "/about");

    cy.get("[data-cy='Posts']").click();
    cy.location("pathname", { timeout: 10000 }).should("include", "/posts");

    cy.get("[data-cy='Contact']").click();
    cy.location("pathname", { timeout: 10000 }).should("include", "/contact");

    cy.get("[data-cy='Sample']").click();
    cy.location("pathname", { timeout: 10000 }).should("include", "/sample");

    cy.get("[data-cy='Home']").click();
    cy.location("pathname", { timeout: 10000 }).should("include", "/");
  });
});
```
</div></details>

#### テストをスキップさせたいとき
E2Eテストは不安定なところがあり、メンテを怠っているといつの間にかテストがこけるようになってしまいます。どうしてもテストを今すぐ改修できない場合は、`xit`というようにitの前に`x`を付与してあげることでそのテストをスキップすることができます。

<details><summary>サンプルコード: テストをスキップする</summary><div>

```js:index.spec.js
/// <reference types="cypress" />

context("/sample - サンプルページ", () => {
  beforeEach(() => {
    cy.visit("/sample");
  });

  it("アクセスしたURLがあっている", () => {
    cy.location("pathname", { timeout: 10000 }).should("include", "/sample");
  });

  xit("このテストは実行されない", () => {
    cy.get("[data-cy=input]").type("123");
    cy.get("[data-cy=output]").contains("123");
  });
});

```
</div></details>

### 余談
エラーが起きる場合はこのように表示されます。このケースだと、同じ要素が２つ以上あるので、clickできないと言われています。今回はその要素で一番最初にヒットしたものをクリックするような処理にしていますが、テストを安定させるためにもできるだけユニークな値で設計していくことが望ましいです。また、`data-cy` と `div` などから取ってくる２つの手法で実装していますが、後者の場合だとdivの重複などが原因でこのエラーのようなケースが発生するので、前者をおすすめします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104635/4c7cb4ce-d707-2189-b34f-a3fef00ddd99.png" width="400px">

## 最後に
E2Eテストは、DB/ネットワーク/デザインの変更/APIの影響を受けて不安定になってしまうことも実際の開発ではよく起こります。そのような不安定なテストとどう向き合うか、他のテストフレームワークと違って何がよいのかなどは別の記事にまとめていきたいと思います。個人的にはNext.jsとCypressの相性がいいなぁと思っているのでその件についても書きたいと思っています。
まとまったら変更通知を送るので気になる方はストックしていただけたらと思います。

<details><summary>アドベントカレンダーのおしらせ</summary><div>
DeNAでは今年、以下の3種のアドベントカレンダーを書いてます！それぞれ違った種類なのでぜひ見てみてください。

[DeNA Advent Calendar 2020](https://qiita.com/advent-calendar/2020/dena)：DeNAエンジニアによるアドベントカレンダー
[DeNA 20 新卒 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/dena-20-shinsotsu)：DeNA 20新卒エンジニアによるアドベントカレンダー
[DeNA 21 新卒 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/dena-21-shinsotsu)：DeNA 21内定者エンジニアによるアドベントカレンダー
この記事を読んで「面白かった」「学びがあった」と思っていただけた方、よろしければ Twitter や Facebook、はてなブックマークにてコメントをお願いします！
また DeNA 公式 Twitter アカウント [@DeNAxTech](https://twitter.com/DeNAxTech) では、 Blog記事だけでなく色々な勉強会での登壇資料も発信してます。ぜひフォローして下さい！
</div></details>