當測試開始左移 feat. React, Playwright and Github Actions

Jersey Su
16 min readJul 11, 2024

--

回顧前一篇測試左移, 裡面有提到幾個概念, 探討如果越早且頻繁地發現軟體的缺陷, 因此開發人員可以更容易地修復問題. 且成本跟壓力都是小的. 並且幾個實際的例子引導出測試左移帶來的好處及最佳實踐.

DevOps from https://www.atulhost.com/what-is-ci-cd

本篇想要延續上面的話題, 繼續跟讀者來看看當測試開始左移時, 我們有哪些實際的應用, 可以幫助團隊開始實踐測試左移.

作者利用 React + Playwright 及 Github Actions 當成前端 end to end 的範例, 環境設定的部分可參考 README.md.

測試左移的實踐

提升可測試性 Increase the testability

當產品需求都釐清, 並且開始增加測試時, 開發及測試人員應該會發現某些功能不太容易進行測試, 因此我們可以在開發階段就考慮可測試性, 例如補上 data-test-locator, 一個小小動作, 就可以讓產品可測試性上升. 筆者使用 React 實作一個登入及註冊的頁面, 並且新增 data-testid 輔助測試.

NG Testability
<div class="MuiFormControl-root MuiFormControl-fullWidth MuiTextField-root css-wb57ya-MuiFormControl-root-MuiTextField-root">
password</div>
Preferred Testability
<div class="MuiFormControl-root MuiFormControl-fullWidth MuiTextField-root css-wb57ya-MuiFormControl-root-MuiTextField-root" 
data-testid="password">password</div>

除了增加 data-test locator 外, 當 debug flag 打開時, 適當的印出 debug message 也是讓測試變容易的方法之一, 輔助我們在測試時候發現特殊的行為, 但如何設計這些 log, 就不在這篇討論的範圍裡面了.

若想執行這個 App, 可以執行下面指令

$ npm run start

靜態程式碼檢查 Lint Check

靜態程式碼檢查是用來標記原始碼中, 某些可疑的, 不具結構性 (可能造成bug) 的段落, 做出語意跟語法的檢查. 語法 syntactic 檢查是用來驗證程式碼中的關鍵字, 對象名稱等是否準確放置, 而語義 semantic 檢查則是用以確定程式碼中的 reference 是否有效. 可以避免許多的人為造成的低級錯誤.

除此之外, 當開發團隊成員漸漸茁壯時, 可以設定相關的檢查規範, 標準化團隊的程式碼, 避免不同習慣導致的錯誤, 甚至可以透過程式碼格式化的工具, 例如: Prettieer 進而統一程式碼風格 (Coding Style). 透過 Git Hook 可以在 PreCommit 的階段就找到問題. 筆者使用 eslint 為範例:

module.exports = {
"extends": [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
"root": true,
"overrides": [
{
files: ['src/**/*.ts', 'src/**/*.tsx']
},
],
"rules": {
"key-spacing": "off",
"@typescript-eslint/key-spacing": "error",
"indent": "off",
"@typescript-eslint/indent": "error"
}
};

範例: 模擬一個簡單的 syntax error

Syntax Error
jerseysu@Jerseys-MacBook-Pro ~/D/playwright_demo> npx eslint /playwright_demo/src/App.tsx

/playwright_demo/src/App.tsx
19:11 error Parsing error: JSX element 'Route' has no corresponding closing tag

✖ 1 problem (1 error, 0 warnings)

若想執行 Lint, 可以執行下面指令

$ npm run lint

單元測試 Unit Test

單元測試 Unit Test 可以幫助我們在簡化的測試環境當中快速的 render dom tree, 並且 assert 頁面的輸出, 雖然它沒辦法像整合測試或 End To End Test (後面會提到)幫助產品了解全盤的狀態, 但是針對每一個小的 component 改動, 我們能針對邏輯或者是輸出進行測試. 筆者使用 Jest 及 React Testing Library 來當作範例.

import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom'
import Login from '../pages/Login';
import { BrowserRouter } from 'react-router-dom'

it('renders login page', () => {
render(<Login />, { wrapper: BrowserRouter });

const emailInputBox = screen.getByTestId('email');
const passwordInputBox = screen.getByTestId('password');

expect(screen.getByText(/Register/i)).toBeInTheDocument();
expect(emailInputBox).toBeInTheDocument();
expect(passwordInputBox).toBeInTheDocument();
});

相關的 test framework 也能夠幫助我們產生對應的測試覆蓋率 (test coverage), 更進一步地數據化每個元件的測試覆蓋率.

---------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------------|---------|----------|---------|---------|-------------------
All files | 36.66 | 0 | 25 | 36.66 |
src | 0 | 0 | 0 | 0 |
App.tsx | 0 | 100 | 0 | 0 | 9-13
index.tsx | 0 | 100 | 100 | 0 | 7-21
reportWebVitals.ts | 0 | 0 | 0 | 0 | 3-10
src/pages | 68.75 | 100 | 37.5 | 68.75 |
Login.tsx | 66.66 | 100 | 33.33 | 66.66 | 47-61
LoginSuccess.tsx | 100 | 100 | 100 | 100 |
Register.tsx | 62.5 | 100 | 25 | 62.5 | 48-74
---------------------|---------|----------|---------|---------|-------------------

Test Suites: 3 passed, 3 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 20.562 s, estimated 23 s
Ran all test suites.

若想執行全部的案例, 可以執行下面指令

$ npm run test

更多可以參考 Jest 及 React Testing Library 的官方文件:

整合測試 Integration Test

整合測試或 End To End Test 可以幫助我們快速了解產品的功能是不是符合預期的使用者行為, 如果要讓這類的測試規律性的發生, 我們可以考慮使用自動化測試來輔助驗證. 筆者使用 Playwright 來當作範例.

當產品的功能及頁面越來越的時候, 自動化測試實踐中我們可以考慮使用 Page Object Pattern 來輔助我們有結構性的管理及維護 Test Suite 和程式碼. Page Object Pattern 可以簡化 Framework API 的使用以及有效率的維護 element selector 及可重複使用程式碼.

範例: 模擬使用登入頁面的測試案例

Page Object Pattern:

import { expect, type Locator, type Page } from '@playwright/test';

export class IndexPage {
readonly page: Page;
readonly loginButton: Locator;
readonly email: Locator;

constructor(page: Page) {
this.page = page;
this.email = page.locator('div[data-testid="email"] input');
this.loginButton = page.locator('a[data-testid="loginButton"]');
}

async fillEmail() {
await this.email.fill('Jersey@devopsday.good');
await expect(this.email).toBeVisible();
}

async action() {
await this.fillEmail();
await this.loginButton.click();
}
}

登入頁面的測試:

import { test, expect } from '@playwright/test';
import { IndexPage } from './pages/indexPage';
import { WelcomePage } from './pages/welcomePage';

test.describe('Login Page', () => {
test('has title', async ({ page }) => {
const indexPage = new IndexPage(page);
await indexPage.goto();

// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Jersey's Login Page/);
});

test('do login', async ({ page }) => {
const indexPage = new IndexPage(page);
await indexPage.goto();
await indexPage.loginAction();

const welcomePage = new WelcomePage(page);
const welcomeMsgText = await welcomePage.getWelcomeMsgText();

// Expect a welcome message LoginSuccess
await expect(welcomeMsgText).toContain('LoginSuccess');
});
});

若想執行全部的案例, 可以執行下面指令

$ npm run test:e2e 

更多可以參考 Playwright 的官方文件:

持續整合及持續部署 CI/CD

持續整合及持續部署為測試左移的重要關鍵之一, 當我們已經有了上述的幾個重要的檢查, Lint Check, Unit Test 及 Integration Test, 我們就能夠將他們都整合進 PR check 中, 試想一下, 如果開發或測試人員來在發 PR 的時候, 就有相關的測試幫助我們頻繁地檢查軟體是否有缺陷, 在非常早期就可以讓開發人員修復問題, 這樣的成本可以說是非常的小, 對於公司或產品品質來說是一大利多, 是一件值得投資的事情.

筆者使用 Github Actions 來當作範例.

name: Playwright-Tests
on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
tests:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Run Unit Test
run: npm run test:ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run test:e2e
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

當 PR 發出來的時候, 每一個 commit 都有相關的檢查, 確保不會有相關的錯誤, 早期發現可以早點治療.

Pull Request with CI

透過 Github Actions, 我們也可以很清楚的知道每一個 CI 的步驟, 確保整個改動事不會影響的.

Pull Request with CI steps

Sample PR include above approach:

當我們的 PR 進入 Main branch 之後, Pipeline 就會往下一個環境推送, 例如: Staging -> Canary -> Production. 在持續部署的過程中, 有了這些測試的把關, 整體對於品質的信心程度也會跟者上升.

Publish to Github Page

最後部署到 Github Page 的頁面

總結

今天提到的下面五點測試左移的實踐, 幾乎都有工具輔助, 因此在實作上面蠻容易找到解決方案, 倘若這些實踐可以應用在讀者的工作環境裡面, 相信各位一定可以有所收穫, 並且大大的提升產品的品質.

  1. 提升可測試性 Increase the testability
  2. 靜態程式碼檢查 Lint Check
  3. 單元測試 Unit Test
  4. 整合測試 Integration Test
  5. 持續整合及持續部署 CI/CD

回顧過去世界的改變, 測試左移的概念已經逐漸深入每個團隊, 在真實世界有許多的挑戰, 筆者的工作環境需要面對跨區域及跨時區的團隊協作, 因此測試左移的實踐無論是軟實力或者是硬實力都已經成為我身體力行的一部分了.每天都要思考如何將測試提早, 幫助公司減少成本, 讓團隊繼續往前邁進.

歡迎各位讀者一起來找我討論, 還有幾個實踐尚未寫在本篇當中. 歡迎一起來討論, 附上程式碼.

Slide

--

--

Jersey Su

我是哲西, 熱愛測試 I am Jersey, I love Software Testing!!!