Kohei Blog

夫・父親・医療系エンジニア

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.containscy.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テスト用リポジトリ