はじめてのE2EテストにCypressを使ってみた時を部分再現

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

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

# 導入してみる

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

```
npm install cypress --save-dev
```
そして実行
```
npx cypress open
```
すると以下のような形でlauncherが開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1911348/0ba0559e-b2e9-3f4c-8636-139da97de72b.png)

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

https://github.com/cypress-io/cypress/issues/23552

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1911348/90410e69-74c3-412b-f22e-f237108e7ca0.png)

Scaffold example specsをクリックすると、手元（./cypress/e2e/）にいくつかサンプルのテストファイルが出来上がる。ここで書き方を学んで実践する感覚。

# テストを作成してみる
今回、以下のリポジトリからサンプルのWebアプリを立ち上げる

https://github.com/tsuemura/fastify-webapp-sample

試しに以下のようなテストを書いてみる
```
/// <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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1911348/55eb50ff-1390-7c9f-a175-543d943f4763.png)

画面左上の「ログインしていません。」のセレクターを知りたい。
そこで、画面上cy.get(body)となっているテキストボックスの左側にあるアイコンを押下し、「ログインしていません。」にむけてカーソルをホバーさせる。
すると、そこのセレクタを示すには"cy.get('span')"でよいと画面に表示される（キャプチャできなかったが）
同様に他２つのケースについても修正し、最終的に以下のようになる
```
/// <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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1911348/ec7379e8-0c3b-c603-28e7-dacab76be647.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')

    //   ３つに変更する
      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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1911348/dc12b128-94d3-1011-c5fc-c8e228eab116.png)

# 感想

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