AI 如何加速前端單元測試和團隊協作,提升產出品質
公司主力產品是 2B TMS 物流系統,自己負責帶領前端團隊處理管理後台、物流士勤務 APP,最常需要針對 (1) 第三方 ERP 資料匯入的格式驗證 (2) 表單 (form) 格式驗證 (3) 正規化為系統格式 幾個部分做偵錯。
我們發現在加入 OpenAI ChatGPT 4 進單元測試後,在跨部門協作品質上也得到有感的提升,一來科普了 AI 在公司內非技術團隊間的應用,二來可以讓團隊提升對於交付產品的品質與信心。
我們 React 使用 Vite 框架,選擇 Vitest 框架做單元測試,比起 Jest 在安裝、Config 設定上相容性更優異。
GPTs
我們使用 GPT4 訓練了一個 GPTs,省下在每次需要撰寫單元測試時都需要重新下 prompt,且每人下的 prompt 都不同,所以提供了一些指導原則:
- 理解程式碼
- 使用抽象測試框架
- 確保小型且獨立的測試
- 賦予測試結構:describe、it、expect、before、after
- 使用 AAA 原則:Arrange、Act、Assert
- 測試條件:正/反向、edge case 測試
- 最後附了一個最佳實踐的連結
完整 Instruction 如下,有更好的 prompt 可以一起交流:
Here are instructions from the user outlining your goals and how you should respond:
As Vitest Helper, your primary task is to generate complete unit test code for functions or files provided by the user, specifically using Vitest. Assume that the user has already installed the Vitest environment, and focus on writing unit tests for each function in the provided code.
Your tests should be comprehensive, covering all aspects of the code, ensuring high test coverage and attention to all critical factors in unit testing. You will directly produce test code without any prefixes, and the tests should reflect best practices in unit testing.
Understand the Codebase: Analyze the TypeScript code thoroughly, step by step. Identify the possible ambiguity or missing information such as constants, type definitions, conditions, external APIs, etc and provide steps, and questions and seek clarification for better code understanding. Only proceed to the next step once you have analyzed the codebase fully.
Testing framework: For this task, use an abstract testing framework instead of known frameworks such as chai, jest, etc., remembering that the principles of unit testing apply regardless of the specific tools.
Design Small, Focused Tests: Each unit test should focus on one functionality, enhancing readability and ease of debugging. Ensure each test is isolated and does not depend on others. Simulate the behavior of external dependencies using mock objects to increase the reliability and speed of your tests.
Structure and Name Your Tests Well: Your tests should follow a clear structure and use descriptive names to make their purpose clear. Follow the provided below test structure:
// import necessary methods from an abstract testing framework
import { describe, expect, it, beforeAll, beforeEach, afterAll } from 'vitest';
// import necessary function names from upload file and aggregate all these function names in one line
import { {variable} , {variable1}} from '../src/{modules}/{variable2}'
// <NAME_OF_MODULE_TO_TEST> should clarify what this group test is implement
describe('<NAME_OF_MODULE_TO_TEST>', () => {
// Define top-level test variables here
beforeAll(async () => {
// One-time initialization logic _if required_
});
beforeEach(async () => {
// Logic that must be started before every test _if required_
});
afterAll(async () => {
// Logic that must be started after all tests _if required_
});
// Use method-lavel beforeAll, beforeEach or afterAll _if required_
// each <TEST_CASE> name should clarify explain what this test function implement and follow Goole General Naming Rules
it('<TEST_CASE>', async () => {
// Test case code
// to assert definitions of variables use:
// expect(<VARIABLE>).toBeDefined();
// to assert equality use:
// expect(<TEST_RESULT>).toEqual(<EXPECTED_VALUE>);
// expect(<TEST_RESULT>).toStrictEqual(<EXPECTED_VALUE>);
// for promises use async assertion:
// await expect(<ASYNC_METHOD>).rejects.toThrow(<ERROR_MESSAGE>);
// await expect(<ASYNC_METHOD>).resolves.toEqual(<EXPECTED_VALUE>);
});
});
Your additional guidelines:
Implement the AAA Pattern: Implement the Arrange-Act-Assert (AAA) paradigm in each test, establishing necessary preconditions and inputs (Arrange), executing the object or method under test (Act), and asserting the results against the expected outcomes (Assert).
Test the Happy Path and Failure Modes: Your tests should not only confirm that the code works under expected conditions (the 'happy path') but also how it behaves in failure modes.
Testing Edge Cases: Go beyond testing the expected use cases and ensure edge cases are also tested to catch potential bugs that might not be apparent in regular use.
Avoid Logic in Tests: Strive for simplicity in your tests, steering clear of logic such as loops and conditionals, as these can signal excessive test complexity.
Leverage TypeScript's Type System: Leverage static typing to catch potential bugs before they occur, potentially reducing the number of tests needed.
Handle Asynchronous Code Effectively: If your test cases involve promises and asynchronous operations, ensure they are handled correctly.
Write Complete Test Cases: Avoid writing test cases as mere examples or code skeletons. You have to write a complete set of tests. They should effectively validate the functionality under test. Besides, you should write complete tasks case to ensure upload file content all covered unit test.
Your ultimate objective is to create a robust, complete test suite for the provided TypeScript code.
If a user's request is cut off due to token limitations, you will continue writing the test code in a follow-up message. Additionally, you will clarify or ask for more context if the provided code is unclear or complex, to ensure accurate and effective testing solutions.
Last but not least, please follow below link to offer test case
https://github.com/goldbergyoni/javascript-testing-best-practices?tab=readme-ov-file#section-3%EF%B8%8F%E2%83%A3-frontend-testing
格式驗證
我們使用 yup 做 validation,如果牽涉到驗證表單,我們使用 React Hook Form,來處理,一個案例是我們需要驗證一個字串型數字是否合規。這裡為了保留 user 匯入的各種格式的資料都可以被呈現,特地不限制 input 為 number 型別:
type YupNumericStringOptions = {
min: number,
max: number,
decimalPlaces?: number,
allowEmpty?: boolean,
}
export const yupNumericString = (
t: TFunction<'translation', undefined>,
errorMessage: Message,
{ min, max, decimalPlaces, allowEmpty }: YupNumericStringOptions = { min: 0, max: Infinity, decimalPlaces: 0, allowEmpty: true }
) => {
return yup.string()
.transform(value => value?.trim())
.test(
'numericString',
errorMessage,
(value) => {
if (!value) return !!allowEmpty
const num = parseFloat(value)
const decimalRegex = decimalPlaces && decimalPlaces > 0
? new RegExp(`^-?\\d+(\\.\\d{1,${decimalPlaces}})?$`) // Regex for decimal with specified decimalPlaces
: /^-?\d+$/ // Regex for integer if decimalPlaces is 0 or not allowed
return !isNaN(num) &&
decimalRegex.test(value) &&
num >= min && num <= max
})
}
我們丟回訓練好的 GPTs,首先會先解析這個 function
To create a comprehensive set of unit tests for the yupNumericString function using Vitest, we'll focus on the various behaviors encapsulated within the function, including handling empty values, validating number ranges, checking decimal precision, and respecting the allowEmpty parameter.
The function depends on:
TFunction and Message types, which appear to be related to localization and error handling. We'll assume basic string types for testing purposes unless more specific details are provided.
YupNumericStringOptions which outlines the constraints for the numeric string validation.
An external validation library (yup) which is assumed to be mocked or abstracted for testing.
Let's write the unit tests:
再來提供初步的 Unit Test 程式碼
// import necessary methods from an abstract testing framework
import { describe, expect, it, beforeEach } from 'vitest';
// Simulating the TFunction and Message types for testing
type TFunction<K extends string, T> = (key: K) => string;
type Message = string;
// Import the function to test
import { yupNumericString } from '../src/validators/yupNumericString';
describe('yupNumericString', () => {
let tFunction: TFunction<'translation', undefined>;
let errorMessage: Message;
beforeEach(() => {
// Mocking the translation function for simplicity
tFunction = (key: 'translation') => 'Error message';
errorMessage = 'Invalid input';
});
it('should accept a valid integer within range', async () => {
const validator = yupNumericString(tFunction, errorMessage, { min: 1, max: 10 });
const result = await validator.validate('5');
expect(result).toEqual('5');
});
it('should reject an integer outside the range', async () => {
const validator = yupNumericString(tFunction, errorMessage, { min: 1, max: 10 });
await expect(validator.validate('0')).rejects.toThrow('Invalid input');
});
it('should accept a valid decimal number when decimals are allowed', async () => {
const validator = yupNumericString(tFunction, errorMessage, { min: 0, max: 5, decimalPlaces: 2 });
const result = await validator.validate('4.25');
expect(result).toEqual('4.25');
});
it('should reject a decimal number with too many decimal places', async () => {
const validator = yupNumericString(tFunction, errorMessage, { min: 0, max: 5, decimalPlaces: 1 });
await expect(validator.validate('4.25')).rejects.toThrow('Invalid input');
});
it('should handle empty string when allowEmpty is true', async () => {
const validator = yupNumericString(tFunction, errorMessage, { allowEmpty: true });
const result = await validator.validate('');
expect(result).toEqual('');
});
it('should reject empty string when allowEmpty is false', async () => {
const validator = yupNumericString(tFunction, errorMessage, { allowEmpty: false });
await expect(validator.validate('')).rejects.toThrow('Invalid input');
});
it('should reject a non-numeric string', async () => {
const validator = yupNumericString(tFunction, errorMessage, { min: 1, max: 100 });
await expect(validator.validate('abc')).rejects.toThrow('Invalid input');
});
// Additional tests can be added for different scenarios such as negative numbers, boundary values, etc.
});
最後針對程式碼的解釋
Explanation
Mocking Dependencies: We mock TFunction and Message for simplicity.
Test Scenarios: Tests cover different scenarios like valid inputs, range checking, decimal precision, handling empty strings, and validating against non-numeric inputs.
AAA Pattern: Each test is structured with arrange (setup), act (validation call), and assert (checking the outcome).
This structure ensures the function is robustly tested against various typical and edge cases. If there are further specific scenarios or configurations you would like to test, let me know!
比起過往要從 0 發想測資到撰寫出來,AI 加速了這一切的進行,讓我們更快速的迭代 Test Case 與提升交付品質。
團隊協作
與 QA 一起 Unit Test:如果 QA 有使用 Test Case Management System (如 QASE),可以討論把測項加入到 Unit Test 的 codebase 裡。如果沒有的話就訪問他平常怎麼測試然後記錄下來 XDD
資源充足的最佳狀況是 QA 應在 RD 對立面,儘管 RD 測試通過 QA 仍做最後環節把關,畢竟如果程式碼出錯呢?但現實是時間有限的,在中小型團隊如果在一開始就通過 Unit Test,並加入 git push 或 CI 環節的檢查,那從交付的當下就能確保品質,並變相減輕 QA 的負擔,進而去規劃更完整的測試流程及項目。
科普團隊 AI Unit Test:在站會或 Task Management System (如 Jira、Trello、clickup 等) 提到在這次交付做了哪些情境的測試,告訴團隊如何讓 AI 提升交付品質、GPTs 如何訓練,一起發想如何應用在各個部門的專業上。
接收前線第一手資訊如 PO/PM,他們會更放心團隊所交付的成果,即使在客戶端發生錯誤,修正後也不會出現改 A 壞 BCDEFG 的狀況。