背景
それまでWebアプリケーション開発においてE2Eテストコードを組んだことがなかったが、機能開発がひと段落したタイミングで何回も大掛かりな変更が入るリスクも低いことから、まあじゃあ入れてみるかというノリで入れた時を思い出しつつ記述。
Cypressって?
- 自動テストフレームワークの一つ。単体テストもできなくはないが、結合テスト・E2Eテストに効果を発揮しやすい
- GUIポチポチを中心に、テストシナリオ通りにコードを組むのが楽
導入してみる
まずcypressをインストールする
npm install cypress --save-dev
そして実行
npx cypress open
E2E Testingをクリックし、指示通りに進めていると以下のような画面にたどり着く
(手元のソースがtypescriptに対応しておらずエラーが発生していたが、cypress.config.tsをjsファイルに変更したら動いた)
Scaffold example specsをクリックすると、手元(./cypress/e2e/)にいくつかサンプルのテストファイルが出来上がる。ここで書き方を学んで実践する感覚。
テストを作成してみる
今回、以下のリポジトリからサンプルのWebアプリを立ち上げる
試しに以下のようなテストを書いてみる
/// <reference types="cypress" />
// Welcome to Cypress!
//
// This spec file contains a variety of sample tests
// for a todo list app that are designed to demonstrate
// the power of writing tests in Cypress.
//
// To learn more about how Cypress works and
// what makes it such an awesome testing tool,
// please read our getting started guide:
// https://on.cypress.io/introduction-to-cypress
describe('example app: first landing', () => {
beforeEach(() => {
// Cypress starts out with a blank slate for each test
// so we must tell it to visit our website with the `cy.visit()` command.
// Since we want to visit the same URL at the start of all our tests,
// we include it in our beforeEach function so that it runs before each test
cy.visit('http://localhost:8080/items')
})
it('displays header as not login mode', () => {
// We use the `cy.get()` command to get all elements that match the selector.
// Then, we use `should` to assert that there are two matched items,
// which are the two default items.
cy.get('/html/body/nav/ul[1]/li[1]/span').should('have.text', 'ログインしていません。')
})
it('can move to login page', () => {
cy.get('/html/body/nav/ul[1]/li[1]/a').click()
cy.url().should('contain', 'login')
})
it('can move to register page', () => {
cy.get('/html/body/nav/ul[1]/li[2]/a').click()
cy.url().should('contain', 'signup')
})
})
画面左上の「ログインしていません。」のセレクターを知りたい。
そこで、画面上cy.get(body)となっているテキストボックスの左側にあるアイコンを押下し、「ログインしていません。」にむけてカーソルをホバーさせる。
すると、そこのセレクタを示すには"cy.get('span')"でよいと画面に表示される(キャプチャできなかったが)
同様に他2つのケースについても修正し、最終的に以下のようになる
/// <reference types="cypress" />
// Welcome to Cypress!
//
// This spec file contains a variety of sample tests
// for a todo list app that are designed to demonstrate
// the power of writing tests in Cypress.
//
// To learn more about how Cypress works and
// what makes it such an awesome testing tool,
// please read our getting started guide:
// https://on.cypress.io/introduction-to-cypress
describe('example app: first landing', () => {
beforeEach(() => {
// Cypress starts out with a blank slate for each test
// so we must tell it to visit our website with the `cy.visit()` command.
// Since we want to visit the same URL at the start of all our tests,
// we include it in our beforeEach function so that it runs before each test
cy.visit('http://localhost:8080/items')
})
it('displays header as not login mode', () => {
// We use the `cy.get()` command to get all elements that match the selector.
// Then, we use `should` to assert that there are two matched items,
// which are the two default items.
cy.get('span').should('have.text', 'ログインしていません。')
})
it('can move to login page', () => {
cy.get(':nth-child(1) > :nth-child(1) > a').click()
cy.url().should('contain', 'login')
})
it('can move to register page', () => {
cy.get('nav > :nth-child(1) > :nth-child(2) > a').click()
cy.url().should('contain', 'signup')
})
})
この調子で商品購入まで行ってみる
/// <reference types="cypress" />
// Welcome to Cypress!
//
// This spec file contains a variety of sample tests
// for a todo list app that are designed to demonstrate
// the power of writing tests in Cypress.
//
// To learn more about how Cypress works and
// what makes it such an awesome testing tool,
// please read our getting started guide:
// https://on.cypress.io/introduction-to-cypress
xdescribe('example app: first landing', () => {
beforeEach(() => {
// Cypress starts out with a blank slate for each test
// so we must tell it to visit our website with the `cy.visit()` command.
// Since we want to visit the same URL at the start of all our tests,
// we include it in our beforeEach function so that it runs before each test
cy.visit('http://localhost:8080/items')
})
it('displays header as not login mode', () => {
// We use the `cy.get()` command to get all elements that match the selector.
// Then, we use `should` to assert that there are two matched items,
// which are the two default items.
cy.get('span').should('have.text', 'ログインしていません。')
})
it('can move to login page', () => {
cy.get(':nth-child(1) > :nth-child(1) > a').click()
cy.url().should('contain', 'login')
})
it('can move to register page', () => {
cy.get('nav > :nth-child(1) > :nth-child(2) > a').click()
cy.url().should('contain', 'signup')
})
})
describe('example app: buy something', () => {
beforeEach(() => {
// Cypress starts out with a blank slate for each test
// so we must tell it to visit our website with the `cy.visit()` command.
// Since we want to visit the same URL at the start of all our tests,
// we include it in our beforeEach function so that it runs before each test
cy.visit('http://localhost:8080/items')
})
it('displays items', () => {
// We use the `cy.get()` command to get all elements that match the selector.
// Then, we use `should` to assert that there are two matched items,
// which are the two default items.
// 商品を2つカートに入れる
cy.get('#quantity-2').type(2)
cy.get(':nth-child(1) > :nth-child(4) > form > [type="submit"]').click()
cy.url().should('contain', 'order')
// 3つに変更する
cy.get('[type="number"]').clear().type(3)
cy.get('[type="number"]').click()
cy.url().should('contain', 'order')
cy.get('[type="number"]').should('have.value', '3')
// さらに別の商品を1つ入れる
cy.get('article > a').click()
cy.url().should('contain', 'item')
cy.get('#quantity-1').type(1)
cy.get(':nth-child(2) > :nth-child(4) > form > [type="submit"]').click()
// 注文確定
cy.url().should('contain', 'order')
cy.get('#fullname').type('俺')
cy.get('#tel').type('0120110110')
cy.get('#orderDate').click().type('2024-12-21')
cy.get('#receiveTime').click().type('11:00')
cy.get(':nth-child(2) > [type="submit"]').click()
cy.get('body > :nth-child(5)').contains('1500円')
})
})
感想
- GUIポチポチで、有効なパスを指定させながらシナリオを進めることができるのは助かる
- 複数タブをサポートしているわけではないので、複数タブを試したければちょっと工夫がいる
- 導入当時は存在を知らなかったというのもあり、playwrightを試していないので機会があったら触ってみたい