3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

背景

それまでWebアプリケーション開発においてE2Eテストコードを組んだことがなかったが、機能開発がひと段落したタイミングで何回も大掛かりな変更が入るリスクも低いことから、まあじゃあ入れてみるかというノリで入れた時を思い出しつつ記述。

Cypressって?

  • 自動テストフレームワークの一つ。単体テストもできなくはないが、結合テスト・E2Eテストに効果を発揮しやすい
  • GUIポチポチを中心に、テストシナリオ通りにコードを組むのが楽

導入してみる

まずcypressをインストールする

npm install cypress --save-dev

そして実行

npx cypress open

すると以下のような形でlauncherが開く
image.png

E2E Testingをクリックし、指示通りに進めていると以下のような画面にたどり着く
(手元のソースがtypescriptに対応しておらずエラーが発生していたが、cypress.config.tsをjsファイルに変更したら動いた)

image.png

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')
    })
})

動かしてみると通らない。xPathの書き方が変な様子。
image.png

画面左上の「ログインしていません。」のセレクターを知りたい。
そこで、画面上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')
    })
})

無事通った
image.png

この調子で商品購入まで行ってみる

/// <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円')
    })
})

無事通った
image.png

感想

  • GUIポチポチで、有効なパスを指定させながらシナリオを進めることができるのは助かる
  • 複数タブをサポートしているわけではないので、複数タブを試したければちょっと工夫がいる
  • 導入当時は存在を知らなかったというのもあり、playwrightを試していないので機会があったら触ってみたい
3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?