捨棄 create-react-app 之餘還架了個 astro blog 昭告天下:e2e 測試

今天介紹的 e2e 測試框架是 cypress,必須先說個人的使用體驗並不算太好,執行時有機率出現誤報(明明畫面已經渲染了預期內容,卻偵測不出來),要在腳本中引用第三方工具套件(比如 dayjs)的方法也不太直覺。但畢竟它還是省了不少手動操作的工,雖不完美但還是多少幫的上一點忙。

如果有使用 Playwright 或是其他 e2e 測試框架經驗的讀者,歡迎分享交流你的開發心得 (;´༎ຶД༎ຶ`)

設定流程

寫下這篇文章時 cypress 版本為 13.0.0,此代從「安裝」到「產生第一份 e2e 腳本」的流程大至如下:

  1. 透過終端安裝(yarn add -D cypress)後,執行 npx cypress run 啟動 cypress 的設定儀表板。之後可以把啟動指令更新到 package.json 或 Makefile 中
  2. 參考官方的儀表板畫面點選左側的 E2E Testing 選項,此時 cypress 會偵測當下的專案是否有任何設定檔,沒有時會幫你自動建立好對應內容
  3. 設定完畢後,專案根目錄應該會出現一個 ./cypress 資料夾(其中包含 fixturessupport)以及 cypress.config.ts
  4. 選擇執行 e2e 測試後,儀表板會接著詢問你要在哪一個環境(Chrome/FireFox/Electron)跑測試。確認環境後,因專案內還沒有任何 e2e 腳本,儀表板會詢問你是否要建立一份新規格。確認建立後,專案中的 ./cypress 中會多一個 e2e 資料夾來讓你放測試腳本

參數設定

完成第一次設定流程後,專案根目錄應該會出現一個 cypress.config.ts 檔(沒有請直接手動新增一份)。

與 webpack 設定哲學一樣,個人習慣從最小可動內容開始,未來有更多需求再慢慢疊加上去:

import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
  },
  retries: 2,
  video: false,
});

設定 baseUrl 後,在 e2e 測試腳本中可直接省略此段內容,即原本的 cy.visit('http://localhost:3000/product?id=xxxxxxx') 可以省略 domain、直接寫 cy.visit('/product?id=xxxxxxx') 即可。

考慮到有時 cypress 就是會有抓不到目標 DOM 的情況,在設定中給予兩次重試機會。錄影(video)則根據實際需求選擇是否開啟。

而沒有在設定檔透過 Preprocessors API 傳入 webpack 設定則是因為 @cypress/webpack-preprocessor 的安裝說明列出依賴套件包含 babel。考慮到整個專案除了 cypress 以外沒有人會用到它,最後決定放棄 alias 的便利性,畢竟在 cypress 腳本中需要大量引用其他模組的情境也比較少。

特殊處理:支援 dayjs

確認 13.0.0 版有效。請先在 ./cypress/support/commands.ts 追加以下內容:

/// <reference types="cypress" />
import dayjs from 'dayjs';

declare global {
  namespace Cypress {
    interface Cypress {
      dayjs(date?: dayjs.ConfigType): dayjs.Dayjs;
    }
  }
}

Cypress.dayjs = dayjs;

接著可以透過 e2e 腳本確認是否設定成功:

describe('[dayjs]', () => {
  it('should be able to use', () => {
    const dayjsString = Cypress.dayjs().format('YYYY-MM-DD');
    const today = new Date().toISOString().split('T')[0];
    expect(dayjsString).to.equal(today);
  });
});

cypress dayjs pass

腳本

cypress 的語法與 jest 類似,基本上測試都是以 describe / it 組成。以下即是一個簡單的「進入 ${baseUrl}/product?id=xxxxxxx 頁,點擊購買按鈕後,購物車中的對應產品數量應等於 1」:

describe('product shopping chart', () => {
  it('should increase item amount accordingly', () => {
    cy.visit('/product?id=xxxxxxx');
    cy.get('.buy').click();
    cy.get('.chart_sum .item-xxxxxxx').should('eq', '1');
  });
});

.should() 中能使用的斷言(assertions)語法可參考 cypress 官方的整理結果

個人經驗是寫腳本大部分的時間會花在透過 .get().contains() 來定位元件,尤其是在使用第三方 UI 套件的情況下(DOM 結構複雜)。這類路徑或是選取器複雜的腳本建議註解不要省,不然三天後大概就不知道是在選畫面上的什麼東西了。

於 Node.js 環境執行

上面透過 npx cypress open 會讓 cypress 開啟儀表板,再讓工程師與儀表板互動來開始測試。而當你只想等 cypress 跑完測試、看看結果就好的時候,則可以參考以下方式來跳過手動的部分:

/* Packages */
import cypress from 'cypress';
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';

/* Data */
import webpackDevelopmentConfig, {
  webpackDevServerConfig,
} from '@/config/webpack.config.development';
import config from '@/cypress.config';

/* Functions */
async function runCypressE2eTest() {
  const compiler = webpack(webpackDevelopmentConfig);
  const devServer = new WebpackDevServer(
    { ...webpackDevServerConfig, open: false },
    compiler
  );

  // 不使用 try catch 捕捉錯誤,當 e2e 出錯時,直接暴露錯誤+停止流程
  await devServer.start();
  await cypress.run({ browser: 'chrome', config });
  await devServer.stop();
}

/* Main */
runCypressE2eTest();

上述 ./script/e2e.ts 在做的事情是:啟動 webpack dev server 後,讓 cypress 在 chrome 環境跑過一遍 e2e 腳本。需要更多說明可參考官方文件

然後在 Makefile 補上以下內容即可在終端輸入 make e2e 執行 cypress e2e 測試了:

.PHONY: e2e
e2e:
	node -r esbuild-runner/register ./script/e2e.ts

恭喜,我們又減少了一些需要手動的工作 (゚ ω ゚)

總結

寫腳本的痛苦多半發生在尋找元件的時候,且在 cypress 中想執行「查詢元件是否存在」的腳本也不太直覺(寫法可參考個人過去文章)。如果有使用其他框架寫 e2e 測試腳本的朋友,歡迎留言分享你們在寫 e2e 測試時最大的困難通常是什麼 (›´ω`‹ )

感謝看到這裡的你。