Bài 20.5: Bài tập thực hành test suite

Bài 20.5: Bài tập thực hành test suite
Chào mừng trở lại với chuỗi bài viết về Lập trình Web Front-end! Sau khi đã tìm hiểu về tầm quan trọng của kiểm thử và các loại kiểm thử khác nhau, đã đến lúc chúng ta xắn tay áo lên và đi sâu vào phần thực hành – cụ thể là làm việc với test suite.
Lý thuyết là một chuyện, nhưng việc áp dụng nó vào thực tế, viết ra những dòng code kiểm thử, và tổ chức chúng một cách hiệu quả lại là chuyện khác. Bài viết này sẽ tập trung hoàn toàn vào việc thực hành xây dựng và quản lý các test suite, giúp bạn làm quen với quy trình và xây dựng sự tự tin khi kiểm thử code của mình.
Test Suite Là Gì Trong Thực Tế?
Nhắc lại một chút, một test suite (bộ kiểm thử) chỉ đơn giản là một tập hợp các bài kiểm thử (test case) có liên quan đến nhau. Tại sao chúng ta lại gom nhóm các test case lại? Có nhiều lý do:
- Tổ chức: Giúp code kiểm thử của bạn gọn gàng, dễ tìm kiếm và dễ hiểu.
- Phạm vi: Thường một test suite sẽ kiểm thử một đơn vị cụ thể của code, ví dụ: một hàm, một component, một module, hoặc một tính năng.
- Chạy cùng nhau: Bạn có thể chạy toàn bộ test suite đó cùng một lúc để đảm bảo rằng đơn vị code tương ứng đang hoạt động chính xác dưới nhiều điều kiện khác nhau.
Trong môi trường phát triển Front-end hiện đại với các framework như React, Next.js, các test suite thường được sử dụng để kiểm thử các component (đảm bảo chúng render đúng, phản ứng với tương tác người dùng) hoặc các utility function (các hàm xử lý logic nghiệp vụ thuần túy).
Tại Sao Phải Thực Hành Test Suite?
Nghe có vẻ đơn giản, chỉ là gom nhóm các test case lại. Nhưng việc thực hành viết và cấu trúc test suite mang lại nhiều lợi ích:
- Làm Quen Với Tooling: Bạn sẽ thực sự gõ code, chạy lệnh, đọc báo cáo kết quả. Điều này giúp bạn làm quen với các framework kiểm thử (như Jest, React Testing Library) một cách tự nhiên nhất.
- Đối Mặt Với Vấn Đề Thật: Lý thuyết không thể lường hết các tình huống phát sinh khi viết test: làm sao để mock dữ liệu? làm sao để kiểm thử tương tác bất đồng bộ? Thực hành sẽ ép bạn tìm hiểu và giải quyết chúng.
- Hiểu Sâu Hơn Về Code Cần Test: Để viết test hiệu quả, bạn buộc phải hiểu cặn kẽ code mình đang test hoạt động như thế nào, các đầu vào có thể có, các đầu ra mong đợi, và các trạng thái khác nhau.
- Xây Dựng Quy Trình: Bạn sẽ hình thành thói quen suy nghĩ về các trường hợp kiểm thử trước hoặc trong khi viết code (Test-Driven Development - TDD là một ví dụ).
- Tăng Tốc Độ Viết Test: Càng thực hành nhiều, bạn càng viết test nhanh hơn, ít sai sót hơn.
Công Cụ Cho Cuộc Hành Trình Thực Hành
Trong các ví dụ dưới đây, chúng ta sẽ chủ yếu sử dụng Jest làm test runner và React Testing Library (RTL) để kiểm thử React Components. Đây là bộ đôi rất phổ biến trong hệ sinh thái React/Next.js.
- Jest: Cung cấp cấu trúc cho test suite (
describe
), test case (it
hoặctest
), các hàm assertions (expect
,toBe
,toEqual
,...). - React Testing Library: Cung cấp các utilities để render component và tương tác với DOM theo cách gần gũi với người dùng nhất (
render
,screen.getByText
,fireEvent
,...).
Cấu Trúc Một Test Suite Cơ Bản Với describe
Trong hầu hết các framework kiểm thử JavaScript phổ biến như Jest, bạn sẽ sử dụng hàm describe
để định nghĩa một test suite.
Cú pháp cơ bản:
describe('Mô tả về đơn vị code đang được kiểm thử', () => {
// Các test case (it/test) sẽ nằm ở đây
});
- Tham số đầu tiên là một chuỗi mô tả. Hãy làm cho nó rõ ràng và đầy đủ để người đọc (và chính bạn sau này) hiểu ngay test suite này đang kiểm thử cái gì.
- Tham số thứ hai là một hàm callback chứa các test case con.
Ví Dụ Thực Hành 1: Kiểm Thử Hàm Thuần Túy (Pure Function)
Hãy bắt đầu với một ví dụ đơn giản nhất: kiểm thử một hàm JavaScript thuần túy (pure function - hàm không có side effect, luôn trả về cùng một kết quả với cùng một đầu vào).
Giả sử chúng ta có một hàm cộng hai số trong file sum.js
:
// src/utils/sum.js
export function sum(a, b) {
return a + b;
}
Bây giờ, chúng ta sẽ tạo một test suite để kiểm thử hàm này. Theo quy ước phổ biến, file test sẽ có cùng tên với file code nhưng thêm đuôi .test.js
hoặc .spec.js
, và thường nằm cùng thư mục hoặc trong thư mục __tests__
.
File sum.test.js
:
// src/utils/sum.test.js
import { sum } from './sum'; // Import hàm cần test
// Định nghĩa một test suite cho hàm sum
describe('Hàm cộng hai số', () => {
// Test case 1: Kiểm thử với hai số dương
it('nên trả về tổng đúng khi cộng hai số dương', () => {
// expect là giá trị thực tế từ code của bạn
// toBe là matcher để so sánh expect với giá trị mong đợi
expect(sum(1, 2)).toBe(3);
});
// Test case 2: Kiểm thử với số âm
it('nên xử lý đúng khi có số âm', () => {
expect(sum(-1, 5)).toBe(4);
expect(sum(0, -10)).toBe(-10);
});
// Test case 3: Kiểm thử với số 0
it('nên trả về số kia khi cộng với 0', () => {
expect(sum(5, 0)).toBe(5);
expect(sum(0, 0)).toBe(0);
});
// Bạn có thể thêm nhiều test case khác cho các trường hợp biên (edge cases)
// Ví dụ: số rất lớn, số thập phân...
});
Giải thích ví dụ:
- Chúng ta dùng
describe('Hàm cộng hai số', ...)
để nhóm tất cả các test liên quan đến hàmsum
vào một test suite. - Bên trong
describe
, mỗi bài kiểm thử độc lập được định nghĩa bằng hàmit
(hoặctest
). Mỗiit
là một test case cụ thể. it('nên trả về tổng đúng khi cộng hai số dương', ...)
: Chuỗi mô tả này phải nói rõ điều gì đang được kiểm thử và kết quả mong đợi là gì.expect(sum(1, 2)).toBe(3);
: Đây là assertion. Chúng ta gọi hàmsum(1, 2)
, bọc kết quả bằngexpect()
, và sử dụng matchertoBe(3)
để khẳng định rằng kết quả thực tế phải bằng 3. Jest cung cấp rất nhiều matcher khác nhau cho các loại so sánh khác nhau (toEqual
,toBeNull
,toBeUndefined
,toBeTruthy
,toBeFalsy
,...).
Bạn có thể chạy test suite này bằng lệnh của Jest (ví dụ: npm test
hoặc yarn test
nếu đã cấu hình). Jest sẽ tìm các file .test.js
hoặc .spec.js
và chạy tất cả các test case trong từng describe
.
Ví Dụ Thực Hành 2: Kiểm Thử Component React Đơn Giản
Kiểm thử component Front-end phức tạp hơn một chút vì liên quan đến DOM, state, props, và tương tác người dùng. React Testing Library giúp việc này trở nên dễ dàng hơn bằng cách tập trung vào việc kiểm thử hành vi của component từ góc nhìn của người dùng.
Hãy tạo một component Button
đơn giản:
// src/components/Button.js
import React from 'react';
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
export default Button;
Bây giờ, chúng ta sẽ viết test suite cho component này trong Button.test.js
:
// src/components/Button.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; // Import matcher mở rộng từ jest-dom
import Button from './Button'; // Import component cần test
// Định nghĩa test suite cho Component Button
describe('Component Button', () => {
// Test case 1: Kiểm thử xem button có render đúng text không
it('nên hiển thị đúng text được truyền vào qua children', () => {
// render component
render(<Button>Click Me</Button>);
// Sử dụng screen để truy vấn các phần tử trong DOM được render
// getByText tìm phần tử theo nội dung text. /Click Me/i là regex không phân biệt hoa thường.
const buttonElement = screen.getByText(/Click Me/i);
// Assertion: Kiểm tra xem phần tử button có tồn tại trong document không
expect(buttonElement).toBeInTheDocument();
});
// Test case 2: Kiểm thử xem hàm onClick có được gọi khi click không
it('nên gọi hàm onClick khi button được click', () => {
// Tạo một mock function bằng Jest.
// Mock function cho phép chúng ta theo dõi xem nó có được gọi không, bao nhiêu lần, với đối số nào...
const handleClick = jest.fn();
// Render component và truyền mock function vào prop onClick
render(<Button onClick={handleClick}>Clickable</Button>);
// Tìm phần tử button
const buttonElement = screen.getByText(/Clickable/i);
// Sử dụng fireEvent để mô phỏng sự kiện click chuột
fireEvent.click(buttonElement);
// Assertion: Kiểm tra xem mock function handleClick có được gọi đúng 1 lần không
expect(handleClick).toHaveBeenCalledTimes(1);
});
// Bạn có thể thêm nhiều test case khác
// Ví dụ: kiểm thử khi button bị disabled, kiểm thử các props khác...
});
Giải thích ví dụ:
- Chúng ta lại dùng
describe('Component Button', ...)
để tạo một test suite cho componentButton
. import { render, screen, fireEvent } from '@testing-library/react';
: Import các hàm cần thiết từ RTL.render
: Dùng để "mount" (gắn) component vào một DOM ảo trong môi trường kiểm thử.screen
: Đối tượng cung cấp các phương thức để truy vấn các phần tử DOM được render (nhưgetByText
,getByRole
,getByLabelText
,...). RTL khuyến khích sử dụngscreen
thay vì kết quả trả về từrender
để gần với cách người dùng tương tác hơn.fireEvent
: Dùng để mô phỏng các sự kiện của người dùng (click, change, keydown,...).
import '@testing-library/jest-dom';
: Import các matcher mở rộng dojest-dom
cung cấp, giúp kiểm thử DOM dễ dàng hơn (toBeInTheDocument
,toHaveClass
,toHaveAttribute
,...).- Test Case 1: Kiểm tra nội dung hiển thị.
render(<Button>Click Me</Button>);
đưa component vào DOM ảo.screen.getByText(/Click Me/i)
tìm phần tử button dựa vào text hiển thị.expect(buttonElement).toBeInTheDocument()
khẳng định rằng phần tử đó đã được render. - Test Case 2: Kiểm thử tương tác.
const handleClick = jest.fn();
tạo một "gián điệp" theo dõi hàmonClick
.render(<Button onClick={handleClick}>Clickable</Button>);
render component với gián điệp đó.fireEvent.click(buttonElement);
mô phỏng việc click.expect(handleClick).toHaveBeenCalledTimes(1);
kiểm tra xem gián điệp có ghi nhận việc hàm được gọi đúng một lần không.
Tổ Chức Các Test Case Trong Một Suite
Như bạn thấy ở các ví dụ trên, một describe
(test suite) có thể chứa nhiều it
(test case). Mỗi it
nên là một bài kiểm thử độc lập cho một trường hợp cụ thể hoặc một hành vi cụ thể của đơn vị code bạn đang test.
Việc chia nhỏ thành nhiều it
giúp:
- Đọc hiểu: Dễ dàng biết từng test case đang kiểm thử điều gì.
- Tìm lỗi: Khi một test case fail, bạn biết chính xác trường hợp nào đang gặp vấn đề.
- Cô lập: Đảm bảo rằng lỗi ở một trường hợp không ảnh hưởng đến các trường hợp khác.
Hãy luôn cố gắng làm cho mô tả của it
trở nên chi tiết và ý nghĩa. Bắt đầu bằng từ "nên" (should) thường là một cách hay: "nên hiển thị...", "nên trả về...", "nên gọi hàm...", "nên có class...".
Tổ Chức Nhiều Test Suite
Khi dự án của bạn lớn dần với nhiều hàm, component, module khác nhau, bạn sẽ có rất nhiều test suite. Cách tổ chức phổ biến nhất là sử dụng cấu trúc thư mục và quy ước đặt tên file:
- Tạo một thư mục
__tests__
ở cấp độ root của dự án hoặc trong các thư mục chứa code nguồn (src
). - Đặt các file test suite cạnh file code mà chúng test, hoặc trong thư mục
__tests__
tương ứng. - Đặt tên file theo format
<tên_file_code>.(test|spec).js/ts/jsx/tsx
.
Ví dụ cấu trúc:
my-react-app/
├── src/
│ ├── components/
│ │ ├── Button.js
│ │ └── Button.test.js <-- Test suite cho Button
│ ├── utils/
│ │ ├── sum.js
│ │ └── sum.test.js <-- Test suite cho sum
│ └── App.js
│ └── App.test.js <-- Test suite cho App
├── __tests__/ <-- Một cách khác để gom tất cả test vào đây
│ └── setupTests.js <-- File cấu hình test (ví dụ: import jest-dom)
└── package.json
Khi bạn chạy lệnh kiểm thử, Jest (hoặc test runner khác) sẽ tự động quét qua các file có đuôi .test.
hoặc .spec.
và chạy tất cả các describe
blocks mà nó tìm thấy.
Một Số Matcher Hay Dùng Khi Thực Hành
Để làm bài tập thực hành của bạn phong phú hơn, hãy khám phá thêm các matcher phổ biến của Jest và jest-dom
:
toBe(value)
: So sánh equality cho các giá trị primitive (số, chuỗi, boolean).toEqual(value)
: So sánh equality cho các đối tượng hoặc mảng (so sánh giá trị bên trong, không phải tham chiếu).toBeNull()
: Kiểm tra giá trị có phải lànull
.toBeUndefined()
: Kiểm tra giá trị có phải làundefined
.toBeTruthy()
: Kiểm tra giá trị có "truthy" (đúng khi chuyển đổi sang boolean).toBeFalsy()
: Kiểm tra giá trị có "falsy" (sai khi chuyển đổi sang boolean).toContain(item)
: Kiểm tra mảng có chứa một phần tử cụ thể không.toHaveLength(number)
: Kiểm tra mảng hoặc chuỗi có độ dài nhất định không.toHaveBeenCalled()
: Kiểm tra mock function có được gọi không.toHaveBeenCalledTimes(number)
: Kiểm tra mock function có được gọi số lần cụ thể không.toHaveBeenCalledWith(...args)
: Kiểm tra mock function có được gọi với đối số cụ thể không.toBeInTheDocument()
(jest-dom): Kiểm tra phần tử DOM có tồn tại trong document không.toBeVisible()
(jest-dom): Kiểm tra phần tử DOM có hiển thị không.toHaveTextContent(text)
(jest-dom): Kiểm tra phần tử DOM có chứa text cụ thể không.toHaveClass(className)
(jest-dom): Kiểm tra phần tử DOM có class cụ thể không.toHaveAttribute(name, value)
(jest-dom): Kiểm tra phần tử DOM có attribute cụ thể không.toHaveValue(value)
(jest-dom): Kiểm tra input, select, textarea có giá trị cụ thể không.
Hãy thực hành sử dụng các matcher này trong các test case của bạn để làm quen với chúng.
Comments