はじめに
【初心者向けワークショップ】Cloud9+React+TypeScript+Amplify+Cypressでアプリの公開からCI/CD+E2Eテスト自動化までやってみよう!でアプリケーションの公開からCI/CD、E2Eのテストまで一通りの流れを体験する記事を書きました。
今回は、アプリケーションを開発しながらE2Eのテストを少しずつ育てていく工程をサクッと体験するチュートリアルを作ってみました。
1時間程度で試すことができる内容にしています!
開発環境の構築
クラウドベースの統合開発環境(IDE)であるAWS Cloud9を使用します。
Cloud9上でDocker Composeが動かせる環境を構築していきます。
CypressはDockerのビルド時にインストールされるため、npmコマンド等のインストールは不要です。
AWSアカウントの取得がまだの方は公式のAWSアカウント作成の流れを参照して
アカウントを取得しておいてください。
AWS Cloud9のセットアップ
- AWS マネジメントコンソールにログインして、サービスからAWS Cloud9を選択する
- 「Create environment」ボタンをクリックする
- Name environmentの項目を入力して(以下は入力例)、「Next Step」ボタンをクリックする
- Configure settingsの項目を入力して(以下は入力例)、「Next Step」ボタンをクリックする
- 設定内容を確認して「Create Environment」をクリックする
AWS Cloud9で利用しているEBSボリューム領域を拡張する
ワークショップを進める上で、Dockerのビルドを行うため、ビルドの実行する工程で領域不足になります。
こちらの記事を参考にして事前にボリュームを拡張しておくことをお勧めします。
私は余裕を持って、10GB -> 20GBに増やしておきました。
AWS Cloud9 で利用しているEBS ボリューム領域を拡張する
必要なパッケージのインストール
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
公式ドキュメントの手順を参考にしてインストールします。
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
サンプルコードの取得
今回はGithubへのコミット、またそれをトリガーにしたCI/CDなどは絡めませんので、GithubリポジトリのForkなどはせず、本家のリポジトリをクローンしてきます。
$ git clone https://github.com/cypress-io/cypress-example-docker-compose.git
$ cd cypress-example-docker-compose
$ git clone git@github.com:cypress-io/cypress-example-docker-compose.git
$ cd cypress-example-docker-compose
コンテナのビルド
Dockerコンテナのビルドを実行します。
$ docker-compose build
が実行されます。
$ 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
が実行されます。
$ 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
嬉しいことその1
勝手に動画を撮って残してくれます。
$ 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」を選択すると閲覧できます。
[e2e/cypress/videos/spec.js.mp4をgifに変換したもの]
嬉しいことその2
Seleniumだと、失敗する可能性がある箇所に page.save_screenshot 'failed.png'
のようなコードを挿入してスクリーンショットを残したりすることがあります。
Cypressは特に工作をしなくてもデフォルトでテスト失敗時には静止画を残してくれます。
動画も残るのでそこからも確認できますが、ズバリここ!みたいなのは静止画の方がわかりやすいです。
以下は、期待結果「Hi there」と表示されることの検証を「Hey there」に変えて失敗させた例です。
$ 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')
})
$ 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」になっていることがバッチリわかりますね!
ワークショップ 〜テストを書きながら開発してみよう〜
テスト駆動開発(Test-Driven Development: TDD)
テスト駆動開発(Test-Driven Development: TDD)とは、テストファーストなプログラムの開発手法です。
つまり、プログラムの実装前にテストコードを書き、そのテストコードに適合するように実装とリファクタリングを進めていく方法を指します。
テスト駆動開発は、「Red」「Green」「Refactoring」という順序で進められます。
テスト駆動開発をすることで以下の利点があります。
- 後工程へバグを持ち越すことを未然に防ぐことができる
- 開発の初期段階で不具合を検知・修正できる
- テストを書く=早い段階で仕様を理解できる
- 開発が進んでいく中で、「そういえばどうなんだっけ。。。」となるのを防げる
- 安心して開発できる
- テストを先に作っておくことで、アプリコードを書いていってもテストを要所要所で実行しておけば壊してないことを常に担保できる
Red
- 実装した機能の要件通りになっていることを担保するテストコードを書く
- テストは失敗する
Green
- どのようなコードでも良いので要件を満たし、テストが成功するコードを書く
- テストは成功する
Refactoring
- テストが成功する状態を維持しつつ簡潔・明快なコードに修正する
実装したい機能のプログラムよりもテストコードを先に書くため、はじめはテストに失敗しますが、プログラムの実装と修正を短いサイクルで何度も繰り返してバグをなくし、正しく動作するコードが書けたらリファクタリングを行います。
ここからは、自由に機能要件を考えていただいてテスト実装、アプリ実装を進めてみてください。
要件の例を挙げておきます。
ぜひ参考にしてオリジナルの仕様を決めて進めてみてください。
Webページの実装にはHTMLやCSSを使用します。
HTMLを書く際には、<meta charset="utf-8"/>
を書き忘れると日本語が文字化けしてしまうのでご注意ください。
また、CSSについては[覚え書き] CSS再入門~セレクタ~をご覧ください。
[要件例]
自己紹介ページを作成する。
自己紹介には、以下が含まれている。
- 名前
- 年齢
- 趣味
[実装するページのイメージ画像]
1. 実装した機能の要件を満たすことを確認するテストを書く
「これ、どうやってテスト書いたらいいんだろう?」という方は、コマンド集も作りましたので参考にしてみてください。
これだけはおさえておきたいCypressコマンド集
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. アプリケーションを実装する
<!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をご覧ください。
$ cypress run --browser chrome
headlessの指定
ヘッドレスで実行したい場合は、以下のように--headless
オプションを指定します。
$ cypress run --browser chrome --headless