E2EテストにCypressを導入する
E2Eテストとは何か?
End to Endテストの略であり、ユーザー利用と同じようにシステム全体を操作して確認するテストのこと
テストを自動化するメリデメ
-
メリット
-
反復的な手作業のコストを削減できる
-
テストの実施コストが下がり、早期にバグチェックがしやすい
-
同じ手順で同じテストを行うため、再現性が高い
-
品質がテスト実行者の習熟度に左右されない
-
-
デメリット
-
テストツールの使い方を学ぶ必要がある
-
自動テストのメンテナンスコストがかかる
-
運用体制が整っていないと無駄になる場合がある
-
テスト自動化はかならずしも品質向上、コスト削減が実現するものではない。
E2Eはシステムの細かな修正1つで壊れることが多く、メンテナンスが大変
Cypressの特徴
-
非同期処理に強い
-
対象のDOMが見つかるまで一定時間単柵を行う
-
アクションが実行された時点でDOMが存在しない場合でもタイムアウトまでにDOMが描画されれば、エラーにならずテストが継続できる
-
待機処理やレスポンスが帰るまでリトライする処理を必要としない
-
-
環境設定が簡単
-
ブラウザやドライバをインストールする必要がない
-
スクリーンショットやビデオ出力、、タイムトラベルなどデバッグを手助けする機能が備わっている
Cypressでテストを構築
install
yarn add cypress
管理画面の起動
npx cypress open
管理画面を起動すると、自動的に /cypress
ディレクトリ配下に自動で構成される。
./
├──cypress
│ ├──fixtures
│ ├──integration
│ ├──plugins
│ └──support
├──node_modules
├──cypress.json
└──packagelock.json
-
cypress/fixtures
-
テストで扱う静的データ
-
fixtures
配下のファイルはテストコードの中で読み込んで使用できる
-
-
cypress/integration
-
テストコードを記述したファイルを
integration
配下に置くことで実行ファイルとして認識される
-
-
cypress/plugins
-
Cypressの裏側で実行されるNode.jsの処理ファイル
-
テストの前後処理やプラグインの構成を記述する
-
-
cypress/support
-
Cypressテストを補助する処理ファイル置き場
-
support
配下のファイルはテスト実行時に1度だけ実行される
-
-
cypress.json
-
環境設定やタイムアウトの設定など
-
Cypressでテストを書く方法
describe()
を使って、一定の単位でテストを宣言
その中で it()
を使ってテストの詳細を書いていく
describe('機能A', () => {
it('正常値入力の確認', () => {
// テスト内容
})
it('異常値入力の確認', () => {
// テスト内容
})
}
Webサイトへアクセス: visit()
遷移先URLを指定し、ブラウザを指定のURLにアクセスさせる。
cypress.json
で baseUrlが定義されていたら、 baseUrl
からの相対パスを指定する。
cy.visit('<https://www.google.com/>')
cy.visit('./login')
操作したいDOMを取得: get()
, contains()
指定のDOMを取得するコマンド
-
get()
-
contains()
-
DOMが包含する文字列かセレクタを指定してDOMを取得
-
文字列などで簡単に取得できる一方で、メッセージの変更でテストが壊れやすいので注意
-
cy.get('.submitButton')
cy.contains('送信')
DOMにアクションを加える: click()
, type()
-
click()
-
クリック操作
-
-
type()
-
キーボード入力
-
cy.contains('送信').click()
cy.get('input').type('山田太郎')
アサーション: should()
BDD、TDDの概念に基づくアサーションコマンド
-
should()
-
宣言した内容が正しいかどうかを判定してアラートを出す
-
cy.contains('山田太郎').should('exist')
cy.get('submitButton').should('be.disabled')
テストの前後処理: beforeEach()
, afterEach()
, before()
, after()
-
beforeEach()
-
afterEach()
itで宣言されたそれぞれのテストに対して、前処理や後処理を定義できる
ログイン処理など、テストに共通する処理をまとめて記述する際に便利
-
before()
-
after()
describe()
で定義されたテスト単位の中で一度だけ実行される前後処理テスト対象であるシステムのデータ更新に便利
describe('機能A', () => {
before(() => {
// 最初に一度だけ実行したい前処理を記述
})
beforeEach(() => {
// 各テストに共通する前処理
})
it('正常値入力の確認', () => {
// テスト内容
})
}
特定のテストのみ実行する: only()
任意のテストのみ実行させるコマンド
開発中のテストコード部分のみテストしたい場合など
describe('機能A', () => {
it.only('正常値入力の確認', () => {
// テスト内容
})
//実行されない↓
it('異常値入力の確認', () => {
// テスト内容
})
}
特定のテストの実行をスキップ: skip()
, xit()
特定のテストをスキップできる
describe('機能A', () => {
//実行されない
it.skip('正常値入力の確認', () => {
// テスト内容
})
//実行されない
xit('異常値入力の確認', () => {
// テスト内容
})
}
簡単なログイン機能をテストする
describe('ログイン機能', () => {
beforeEach(() => {
cy.visit('<http://localhost:3000>')
})
it('正常値入力の確認', () => {
cy.get('.inputName').type('山田太郎')
cy.get('.inputPassword').type('yamadaPass1234')
cy.get('.button').click()
cy.contains('成功しました').should('exist')
})
})
GUIを使ったテスト
npx cypress open
CLIを使ったテスト
npx cypress run
ToDoアプリのテスト
// 関数として定義して使い回す
const addTodo = (value) => {
// valueを入力してenterキーを発火させる
cy.get('.new-todo')
.type(value)
.should('have.value', value)
.type('{enter}', { delay: 100 })
// 入力欄に値が無いことを確認する
cy.get('.new-todo').should('have.value', '')
// コンテンツがレンダリングされていることを確認する
cy.contains(value)
}
const deleteTodo = (nth) => {
// .invoke('show') でホバーコンテンツを表示させてクリック
cy.get(`.todo-list > li:nth(${nth}) .destroy`).invoke('show').click()
}
describe('ToDoアプリ', () => {
beforeEach(() => {
cy.visit('<https://example.cypress.io/todo>')
})
it('add 3 todo and delete middle todo', () => {
addTodo('todo1')
addTodo('todo2')
addTodo('todo3')
deleteTodo(3) // 2つ目を削除
})
})
CustomCommandsでコマンドを使い回す
複数のspecファイルで使用する可能性がある場合、CustomCommandsで定義する
時分のほしいコマンドを cy.contains
や cy.get
と同じように作成できる
cypress/support/commands.js
// cy.addTodoとして呼び出せる
Cypress.Commands.add('addTodo', (value) => {
cy.get('.new-todo')
.type(value)
.should('have.value', value)
.type('{enter}', { delay: 100 })
// 入力欄に値が無いことを確認する
cy.get('.new-todo').should('have.value', '')
// コンテンツがレンダリングされていることを確認する
cy.contains(value)
})
// cy.deleteTodoとして呼び出せる
Cypress.Commands.add('deleteTodo', (nth) => {
cy.get(`.todo-list > li:nth(${nth}) .destroy`).invoke('show').click()
})
Jestとの使い分け
Cypressのメリット
-
Cypress固有の自動リトライにより、ウェイト処理といった低水準な処理が少ない高水準なテストが書ける
-
失敗時に「こういうロールはあるがこれはなかった」といったエラーが出るのがわかりやすい。それぞれのステップのDOMツリーを見ながらデバッグできる
-
完成品のWeb画面へのテスト、とくに画面遷移を含んだテストがやりやるい
Jestのメリット
-
圧倒的に高速
-
ReactHooksのテストなど、テスト用カスタムコンポーネントを作るのが楽
Jestでユニットテストを書いてカバレッジを上げつつ、Cypressを使っていくのがいい。
cypressテスト用リポジトリ