【初心者向けワークショップ】Cloud9＋Docker Compose＋CypressでE2Eテストを書いてみよう！

# はじめに
[【初心者向けワークショップ】Cloud9+React+TypeScript+Amplify+Cypressでアプリの公開からCI/CD+E2Eテスト自動化までやってみよう！](https://qiita.com/oh_rusty_nail/items/90f19b14fe12b65f0ed8)でアプリケーションの公開からCI/CD、E2Eのテストまで一通りの流れを体験する記事を書きました。
今回は、アプリケーションを開発しながらE2Eのテストを少しずつ育てていく工程をサクッと体験するチュートリアルを作ってみました。
１時間程度で試すことができる内容にしています！

# 開発環境の構築
クラウドベースの統合開発環境(IDE)であるAWS Cloud9を使用します。
Cloud9上でDocker Composeが動かせる環境を構築していきます。
CypressはDockerのビルド時にインストールされるため、npmコマンド等のインストールは不要です。

AWSアカウントの取得がまだの方は公式の[AWSアカウント作成の流れ](https://aws.amazon.com/jp/register-flow/)を参照して
アカウントを取得しておいてください。

## AWS Cloud9のセットアップ

* AWS マネジメントコンソールにログインして、サービスからAWS Cloud9を選択する 
    * リージョン：東京
    * ![cloud9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/2993e399-df38-4d51-3e18-a12736ebda8a.png)
* 「Create environment」ボタンをクリックする 
    * ![cloud9_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/0f1171da-5a61-4db6-f624-27c86e0ca905.png)
* Name environmentの項目を入力して(以下は入力例)、「Next Step」ボタンをクリックする
    * Name : e2e-workshop
    * Description : cypress E2E test workshop
    * ![スクリーンショット 2020-09-09 22.40.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/90019ff8-78b6-aeb3-0dba-c6f8e469cc04.png)
* Configure settingsの項目を入力して(以下は入力例)、「Next Step」ボタンをクリックする
    * Environment type : Create a new EC2 instance for environment (direct access)
    * Instance type : t2.small
        * Other instance typeから選択
    * Platform : Amazon Linux
    * Cost-saving setting : after 30 minutes (default)
    * ![スクリーンショット 2020-09-09 22.44.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/7937d7bf-c636-c39e-d9bf-3c0b242cf228.png)
* 設定内容を確認して「Create Environment」をクリックする 
    * ![スクリーンショット 2020-09-09 22.46.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/bf0af958-a626-0cab-a0eb-1f76b95ffb99.png)
    * しばらくして以下のような画面が表示されれば完了
        * ![スクリーンショット 2020-09-09 22.49.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/603885f5-2a60-6f33-d466-c1f1239dfd95.png)

## AWS Cloud9で利用しているEBSボリューム領域を拡張する
ワークショップを進める上で、Dockerのビルドを行うため、ビルドの実行する工程で領域不足になります。
こちらの記事を参考にして事前にボリュームを拡張しておくことをお勧めします。
私は余裕を持って、10GB -> 20GBに増やしておきました。
[AWS Cloud9 で利用しているEBS ボリューム領域を拡張する](https://qiita.com/ktrkmk/items/8cf1e100da2e717f3be2)

## 必要なパッケージのインストール

### Docker Compose
Dockerは標準装備されていますが、Docker Composeは入っていないためインストールする必要があります。(2020/09/09時点)

```
ec2-user:~/environment $ docker -v
Docker version 19.03.6-ce, build 369ce74
ec2-user:~/environment $ docker-compose -v
bash: docker-compose: command not found
```

[公式ドキュメント](https://docs.docker.com/compose/install/)の手順を参考にしてインストールします。

#### Docker Composeの現在の安定リリースバージョンをダウンロード
以下は、2020/09/09時点での最新を取得しています。

```
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.27.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ docker-compose -v
docker-compose version 1.27.0, build 980ec85b
```

# Cypressを使ったE2Eテストを試すサンプルコードの取得
Dockerを使ったローカルのアプリケーションに対してCypressでE2Eテストを行えるサンプルコードがGithubで公開されていますので、今回はこちらを利用します。
[https://github.com/cypress-io/cypress-example-docker-compose](https://github.com/cypress-io/cypress-example-docker-compose)

## サンプルコードの取得
今回はGithubへのコミット、またそれをトリガーにしたCI/CDなどは絡めませんので、GithubリポジトリのForkなどはせず、本家のリポジトリをクローンしてきます。

```:httpsの場合
$ git clone https://github.com/cypress-io/cypress-example-docker-compose.git
$ cd cypress-example-docker-compose
```

```:sshの場合
$ git clone git@github.com:cypress-io/cypress-example-docker-compose.git
$ cd cypress-example-docker-compose
```

# コンテナのビルド
Dockerコンテナのビルドを実行します。
`$ docker-compose build` が実行されます。

```bash
$ npm run build

> cypress-example-docker-compose@1.0.0 build /home/ec2-user/environment/cypress-example-docker-compose
> docker-compose build

Building web
Step 1/4 : FROM httpd:2.4
 ---> a6ea92c35c43
Step 2/4 : RUN echo "ServerName localhost" >> /usr/local/apache2/conf/httpd.conf
 ---> Using cache
 ---> e132eaf0b6d6
Step 3/4 : COPY index.html /usr/local/apache2/htdocs/
 ---> Using cache
 ---> a79afef5fb17
Step 4/4 : EXPOSE 80
 ---> Using cache
 ---> 0ba9c6797d6d

Successfully built 0ba9c6797d6d
Successfully tagged apache:latest
Building e2e
Step 1/7 : FROM cypress/base:10
10: Pulling from cypress/base
d6ff36c9ec48: Pull complete
c958d65b3090: Pull complete
edaf0a6b092f: Pull complete
80931cf68816: Pull complete
bc1b8aca3825: Pull complete
ad9790d89c32: Pull complete
6085b6a0249c: Pull complete
6af9e71c78d2: Pull complete
d85bae49b22d: Pull complete
f6c8ce594b00: Pull complete
d67d7860a80a: Pull complete
b3a1dfd049d1: Pull complete
6fb47a9e5454: Pull complete
Digest: sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Status: Downloaded newer image for cypress/base:10
 ---> 071155d6ed07
Step 2/7 : WORKDIR /app
 ---> Running in 468f37420452
Removing intermediate container 468f37420452
 ---> 2f1c6d19e291
Step 3/7 : COPY package.json .
 ---> 90ec115cb213
Step 4/7 : COPY package-lock.json .
 ---> 54e720d60a87
Step 5/7 : ENV CI=1
 ---> Running in 5e56a602e37c
Removing intermediate container 5e56a602e37c
 ---> 670f913b90e6
Step 6/7 : RUN npm ci
 ---> Running in d511a99685c5

> cypress@5.1.0 postinstall /app/node_modules/cypress
> node index.js --exec install

[14:40:14]  Downloading Cypress     [started]
[14:40:17]  Downloading Cypress     [completed]
[14:40:17]  Unzipping Cypress       [started]
[14:40:33]  Unzipping Cypress       [completed]
[14:40:33]  Finishing Installation  [started]
[14:40:33]  Finishing Installation  [completed]
added 216 packages in 25.818s
Removing intermediate container d511a99685c5
 ---> 8dc49d0f8503
Step 7/7 : RUN npx cypress verify
 ---> Running in aa9a7fb8a0d9
[14:41:10]  Verifying Cypress can run /root/.cache/Cypress/5.1.0/Cypress [started]
[14:41:14]  Verifying Cypress can run /root/.cache/Cypress/5.1.0/Cypress [completed]
Removing intermediate container aa9a7fb8a0d9
 ---> 0a5f502ade2e

Successfully built 0a5f502ade2e
Successfully tagged cypress:latest
```

# Webアプリケーションの起動とCypressを使ったE2Eテストの実行
まず、何も手を入れない状態でテストが動くことを確認しましょう。
そして、Cypressを使うとどんな嬉しいことがあるのか見てみましょう。
`$ docker-compose build` が実行されます。

```bash
$ npm run up

> cypress-example-docker-compose@1.0.0 up /home/ec2-user/environment/cypress-example-docker-compose
> docker-compose up --abort-on-container-exit --exit-code-from e2e

Creating network "cypress-example-docker-compose_default" with the default driver
Creating apache ... done
Creating cypress ... done
Attaching to apache, cypress
apache | [Wed Sep 09 14:44:05.645451 2020] [mpm_event:notice] [pid 1:tid 140649514787968] AH00489: Apache/2.4.46 (Unix) configured -- resuming normal operations
apache | [Wed Sep 09 14:44:05.664458 2020] [core:notice] [pid 1:tid 140649514787968] AH00094: Command line: 'httpd -D FOREGROUND'
cypress | 
cypress | ====================================================================================================
cypress | 
cypress |   (Run Starting)
cypress | 
cypress |   ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
cypress |   │ Cypress:    5.1.0                                                                              │
cypress |   │ Browser:    Electron 83 (headless)                                                             │
cypress |   │ Specs:      1 found (spec.js)                                                                  │
cypress |   └────────────────────────────────────────────────────────────────────────────────────────────────┘
cypress | 
cypress | 
cypress | ────────────────────────────────────────────────────────────────────────────────────────────────────
cypress |                                                                                                     
cypress |   Running:  spec.js                                                                         (1 of 1)
apache | 172.18.0.3 - - [09/Sep/2020:14:44:15 +0000] "GET / HTTP/1.1" 200 27
cypress | 
cypress |   (Results)
cypress | 
cypress |   ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
cypress |   │ Tests:        1                                                                                │
cypress |   │ Passing:      1                                                                                │
cypress |   │ Failing:      0                                                                                │
cypress |   │ Pending:      0                                                                                │
cypress |   │ Skipped:      0                                                                                │
cypress |   │ Screenshots:  0                                                                                │
cypress |   │ Video:        true                                                                             │
cypress |   │ Duration:     0 seconds                                                                        │
cypress |   │ Spec Ran:     spec.js                                                                          │
cypress |   └────────────────────────────────────────────────────────────────────────────────────────────────┘
cypress | 
cypress | 
cypress |   (Video)
cypress | 
cypress |   -  Started processing:  Compressing to 32 CRF                                                     
cypress |   -  Finished processing: /app/cypress/videos/spec.js.mp4                                 (1 second)
cypress | 
cypress | 
cypress | ====================================================================================================
cypress | 
cypress |   (Run Finished)
cypress | 
cypress | 
cypress |        Spec                                              Tests  Passing  Failing  Pending  Skipped  
cypress |   ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
cypress |   │ ✔  spec.js                                  342ms        1        1        -        -        - │
cypress |   └────────────────────────────────────────────────────────────────────────────────────────────────┘
cypress |     ✔  All specs passed!                        342ms        1        1        -        -        -  
cypress | 
cypress exited with code 0
Aborting on container exit...
Stopping apache  ... done
```

### 嬉しいことその１
勝手に動画を撮って残してくれます。

```:動画の保存場所
$ ls -l e2e/cypress/videos/spec.js.mp4
-rw-r--r-- 1 root root 29169 Sep  9 14:44 e2e/cypress/videos/spec.js.mp4
```

AWS Cloud9で動画や画像を閲覧する際は、以下のようにファイルにマウスカーソルをフォーカスさせて、右クリックで「Preview」を選択すると閲覧できます。
![スクリーンショット 2020-09-10 22.30.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/6848675f-2a1f-a3e3-4933-87cf1cbab1d1.png)
![スクリーンショット 2020-09-10 22.35.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/c5c1d69a-525a-4cd5-2a33-3b992620ef4d.png)

#### [e2e/cypress/videos/spec.js.mp4をgifに変換したもの]
![spec.js.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/34cb99dc-31b5-1def-fa55-f687f94815ed.gif)

### 嬉しいことその2
Seleniumだと、失敗する可能性がある箇所に `page.save_screenshot 'failed.png'` のようなコードを挿入してスクリーンショットを残したりすることがあります。
Cypressは特に工作をしなくてもデフォルトでテスト失敗時には静止画を残してくれます。
動画も残るのでそこからも確認できますが、ズバリここ！みたいなのは静止画の方がわかりやすいです。
以下は、期待結果「Hi there」と表示されることの検証を「Hey there」に変えて失敗させた例です。

```bash
$ git diff
diff --git a/e2e/cypress/integration/spec.js b/e2e/cypress/integration/spec.js
index 5e90f2d..809de14 100644
--- a/e2e/cypress/integration/spec.js
+++ b/e2e/cypress/integration/spec.js
@@ -1,4 +1,4 @@
 it('loads page', () => {
   cy.visit('/')
-  cy.contains('Hi there')
+  cy.contains('Hey there')
 })
```

```bash
$ npm run up

> cypress-example-docker-compose@1.0.0 up /home/ec2-user/environment/cypress-example-docker-compose
> docker-compose up --abort-on-container-exit --exit-code-from e2e

Starting apache ... done
Starting cypress ... done
Attaching to apache, cypress
apache | [Wed Sep 09 14:55:49.753051 2020] [mpm_event:notice] [pid 1:tid 140399861245056] AH00489: Apache/2.4.46 (Unix) configured -- resuming normal operations
apache | [Wed Sep 09 14:55:49.753761 2020] [core:notice] [pid 1:tid 140399861245056] AH00094: Command line: 'httpd -D FOREGROUND'
cypress | 
cypress | ====================================================================================================
cypress | 
cypress |   (Run Starting)
cypress | 
cypress |   ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
cypress |   │ Cypress:    5.1.0                                                                              │
cypress |   │ Browser:    Electron 83 (headless)                                                             │
cypress |   │ Specs:      1 found (spec.js)                                                                  │
cypress |   └────────────────────────────────────────────────────────────────────────────────────────────────┘
cypress | 
cypress | 
cypress | ────────────────────────────────────────────────────────────────────────────────────────────────────
cypress |                                                                                                     
cypress |   Running:  spec.js                                                                         (1 of 1)
apache | 172.18.0.3 - - [09/Sep/2020:14:55:59 +0000] "GET / HTTP/1.1" 200 27
cypress | 
cypress |   (Results)
cypress | 
cypress |   ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
cypress |   │ Tests:        1                                                                                │
cypress |   │ Passing:      0                                                                                │
cypress |   │ Failing:      1                                                                                │
cypress |   │ Pending:      0                                                                                │
cypress |   │ Skipped:      0                                                                                │
cypress |   │ Screenshots:  1                                                                                │
cypress |   │ Video:        true                                                                             │
cypress |   │ Duration:     4 seconds                                                                        │
cypress |   │ Spec Ran:     spec.js                                                                          │
cypress |   └────────────────────────────────────────────────────────────────────────────────────────────────┘
cypress | 
cypress | 
cypress |   (Screenshots)
cypress | 
cypress |   -  /app/cypress/screenshots/spec.js/loads page (failed).png                             (1280x720)
cypress | 
cypress | 
cypress |   (Video)
cypress | 
cypress |   -  Started processing:  Compressing to 32 CRF                                                     
cypress |   -  Finished processing: /app/cypress/videos/spec.js.mp4                                (2 seconds)
cypress | 
cypress | 
cypress | ====================================================================================================
cypress | 
cypress |   (Run Finished)
cypress | 
cypress | 
cypress |        Spec                                              Tests  Passing  Failing  Pending  Skipped  
cypress |   ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
cypress |   │ ✖  spec.js                                  00:04        1        -        1        -        - │
cypress |   └────────────────────────────────────────────────────────────────────────────────────────────────┘
cypress |     ✖  1 of 1 failed (100%)                     00:04        1        -        1        -        -  
cypress | 
cypress exited with code 1
```

```:テスト失敗時に撮影される静止画の保存場所
$ ls -l e2e/cypress/screenshots/spec.js/loads\ page\ \(failed\).png 
-rw-r--r-- 1 root root 29790 Sep  9 14:56 e2e/cypress/screenshots/spec.js/loads page (failed).png
```

#### [e2e/cypress/screenshots/spec.js/loads\ page\ \(failed\).png ]
「Hi there」を期待しているのに「Hey there」になっていることがバッチリわかりますね！
![スクリーンショット 2020-09-10 0.05.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/96cb15cc-bea5-3658-a19e-ec1a80e2bce9.png)

# ワークショップ 〜テストを書きながら開発してみよう〜
## テスト駆動開発(Test-Driven Development: TDD)
テスト駆動開発（Test-Driven Development: TDD）とは、テストファーストなプログラムの開発手法です。
つまり、プログラムの実装前にテストコードを書き、そのテストコードに適合するように実装とリファクタリングを進めていく方法を指します。
テスト駆動開発は、「Red」「Green」「Refactoring」という順序で進められます。
テスト駆動開発をすることで以下の利点があります。

* 後工程へバグを持ち越すことを未然に防ぐことができる
    * 開発の初期段階で不具合を検知・修正できる
* テストを書く＝早い段階で仕様を理解できる
    * 開発が進んでいく中で、「そういえばどうなんだっけ。。。」となるのを防げる
* 安心して開発できる
    * テストを先に作っておくことで、アプリコードを書いていってもテストを要所要所で実行しておけば壊してないことを常に担保できる

### Red
* 実装した機能の要件通りになっていることを担保するテストコードを書く
* テストは**失敗**する

### Green
* どのようなコードでも良いので要件を満たし、テストが成功するコードを書く
* テストは**成功**する

### Refactoring
* テストが成功する状態を維持しつつ簡潔・明快なコードに修正する

実装したい機能のプログラムよりもテストコードを先に書くため、はじめはテストに失敗しますが、プログラムの実装と修正を短いサイクルで何度も繰り返してバグをなくし、正しく動作するコードが書けたらリファクタリングを行います。

ここからは、自由に機能要件を考えていただいてテスト実装、アプリ実装を進めてみてください。
要件の例を挙げておきます。
ぜひ参考にしてオリジナルの仕様を決めて進めてみてください。
Webページの実装にはHTMLやCSSを使用します。
HTMLを書く際には、`<meta charset="utf-8"/>`を書き忘れると日本語が文字化けしてしまうのでご注意ください。
また、CSSについては[[覚え書き] CSS再入門～セレクタ～](https://qiita.com/oh_rusty_nail/items/e522fe74bc3547b3e826)をご覧ください。


## [要件例]
自己紹介ページを作成する。
自己紹介には、以下が含まれている。

* 名前
* 年齢
* 趣味

### [実装するページのイメージ画像]
![スクリーンショット 2020-09-12 23.16.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19778/3da2690b-ccab-3a07-68f5-e78b283153d2.png)



## 1. 実装した機能の要件を満たすことを確認するテストを書く
「これ、どうやってテスト書いたらいいんだろう？」という方は、コマンド集も作りましたので参考にしてみてください。
[これだけはおさえておきたいCypressコマンド集](https://qiita.com/oh_rusty_nail/items/58dcec335d67e81816dd)


```javascript:e2e/cypress/integration/spec.js
it('load page', () => {
  cy.visit('/')
  cy.get('.title').should('have.text', 'RustyNailの部屋')
  cy.get('.summary').should('have.text', 'これはRusty Nailの自己紹介のページです。')
  cy.get('.content__name').should('have.text', 'Rusty Nail')
  cy.get('.content__age').should('have.text', '34歳')
  cy.get('.content__hobby').should('have.text', '卓球')
})
```

## 2. テストを実行する(Red : 失敗する)
`$ npm run up`を実行して、テストを走らせます。
まだ何もテストを書いていないため当然失敗します。

## 3. アプリケーションを実装する

```html:webapp/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>RustyNailの部屋</title>
</head>
<body>
  <h1 class='title'>RustyNailの部屋</h1>
  <h2 class='summary'>これはRusty Nailの自己紹介のページです。</h2>

  <ul class='content'>
    <li class='content__name'>Rusty Nail</li>
    <li class='content__age'>34歳</li>
    <li class='content__hobby'>卓球</li>
  </ul>
</body>
</html>
```

`$ npm run build` を実行して、webapp/index.htmlの変更をDockerイメージに更新します。

## 4. テストを実行する(Green : 成功する)
`$ npm run up`を再度実行して、テストを走らせます。
要件を満たすアプリケーションの実装も完了したため、テストは成功します。

このような感じで、次は「特技」「好きな食べ物」など追加していく場合、
1つテストを書いて1つ実装を繰り返していきます。

# おまけ
ワークショップでは、デフォルトブラウザを使用しましたが、各種ブラウザがインストールされていれば指定することができます。

```:使用した(デフォルト)ブラウザ
cypress |   ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
cypress |   │ Cypress:    5.1.0                                                                              │
cypress |   │ Browser:    Electron 83 (headless)                                                             │
cypress |   │ Specs:      1 found (spec.js)                                                                  │
cypress |   └────────────────────────────────────────────────────────────────────────────────────────────────┘
```

ワークショップでは、ブラウザのインストールなどはDocker環境を整備することになるため割愛しています。

## 実行するブラウザの指定
以下のようにブラウザを指定できます。
対応ブラウザの詳細は、公式の[Launching Browsers](https://docs.cypress.io/guides/guides/launching-browsers.html#Browsers)をご覧ください。

```:chromeを指定した例
$ cypress run --browser chrome
```

## headlessの指定
ヘッドレスで実行したい場合は、以下のように`--headless`オプションを指定します。

```
$ cypress run --browser chrome --headless
```