Unit test for react component

Unit test for react component

📚 Các khái niệm chính của Jest & RTL

Jest

  • describe(name, fn): Gom nhóm các bài test có liên quan lại với nhau. Giống như một "bộ test" (test suite).

  • test(name, fn) hoặc it(name, fn): Định nghĩa một trường hợp kiểm thử cụ thể (test case).

  • expect(value): Tạo một "khẳng định" (assertion). Bạn sẽ dùng nó cùng với các "matcher".

  • Matchers: Các hàm để kiểm tra giá trị. Ví dụ:

    • .toBe(value): So sánh giá trị chính xác (cho kiểu nguyên thủy).

    • .toEqual(value): So sánh giá trị của object hoặc array.

    • .toBeInTheDocument(): Kiểm tra một element có tồn tại trong DOM không (từ @testing-library/jest-dom).

    • .toHaveBeenCalled(): Kiểm tra một hàm mock đã được gọi hay chưa.

React Testing Library (RTL)

  • render(): "Vẽ" (render) component của bạn vào một DOM ảo để kiểm thử.

  • screen: Một đối tượng cung cấp các phương thức để truy vấn (query) các element trên DOM ảo vừa được render. Ví dụ: screen.getByText('Submit').

  • fireEvent: Dùng để mô phỏng các sự kiện của người dùng như click, change, submit...


📝 Hướng dẫn từng bước viết Unit Test

Hãy cùng viết test cho một component Counter đơn giản.

Bước 1: Tạo Component cần test

Tạo file src/components/Counter.js:

JavaScript
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Counter</h1>
      <p>Current count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}

export default Counter;

Bước 2: Tạo file Test

Theo quy ước, file test sẽ có tên là [TênComponent].test.js hoặc [TênComponent].spec.js và đặt cùng cấp với file component.

Tạo file src/components/Counter.test.js:

JavaScript
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; // Thêm các matcher hữu ích cho DOM
import Counter from './Counter';

// Bắt đầu một bộ test cho component Counter
describe('<Counter />', () => {

  // Test case 1: Kiểm tra component render đúng trạng thái ban đầu
  test('renders the initial state correctly', () => {
    // 1. Render component
    render(<Counter />);

    // 2. Tìm các element quan trọng bằng các truy vấn của 'screen'
    const titleElement = screen.getByText(/counter/i); // Tìm text chứa "counter", không phân biệt hoa thường
    const countElement = screen.getByText(/current count: 0/i);
    const incrementButton = screen.getByRole('button', { name: /increment/i }); // Tìm button có tên "Increment"

    // 3. Khẳng định (Assert) rằng các element này tồn tại trong DOM
    expect(titleElement).toBeInTheDocument();
    expect(countElement).toBeInTheDocument();
    expect(incrementButton).toBeInTheDocument();
  });

  // Test case 2: Kiểm tra hành vi khi người dùng nhấn nút "Increment"
  test('increments the count when the increment button is clicked', () => {
    // 1. Render component
    render(<Counter />);

    // 2. Tìm nút increment
    const incrementButton = screen.getByRole('button', { name: /increment/i });

    // 3. Mô phỏng hành động click của người dùng
    fireEvent.click(incrementButton);

    // 4. Tìm element hiển thị count và khẳng định rằng nó đã được cập nhật lên 1
    const countElement = screen.getByText(/current count: 1/i);
    expect(countElement).toBeInTheDocument();
  });

  // Test case 3: Kiểm tra hành vi khi người dùng nhấn nút "Decrement"
  test('decrements the count when the decrement button is clicked', () => {
    // 1. Render component
    render(<Counter />);

    // 2. Tìm nút decrement
    const decrementButton = screen.getByRole('button', { name: /decrement/i });

    // 3. Mô phỏng hành động click
    fireEvent.click(decrementButton);

    // 4. Khẳng định rằng count đã được cập nhật thành -1
    const countElement = screen.getByText(/current count: -1/i);
    expect(countElement).toBeInTheDocument();
  });
});

Bước 3: Chạy Test

Mở terminal và chạy lệnh:

Bash
npm test

Jest sẽ tự động tìm tất cả các file có đuôi .test.js hoặc .spec.js và thực thi chúng. Bạn sẽ thấy kết quả báo cáo rằng cả 3 test case đều đã "PASS".


✨ Các kịch bản phổ biến khác

1. Test Props

Làm sao để test component nhận props? Rất đơn giản, chỉ cần truyền props vào khi render.

Component Greeting.js:

JavaScript
function Greeting({ name }) {
  return <p>Hello, {name}!</p>;
}

Test Greeting.test.js:

JavaScript
test('renders a greeting with the provided name', () => {
  render(<Greeting name="Gemini" />);
  const greetingElement = screen.getByText(/Hello, Gemini!/i);
  expect(greetingElement).toBeInTheDocument();
});

2. Test sự kiện và Mock Function

Khi một button được click, nó sẽ gọi một hàm được truyền qua props. Ta có thể dùng jest.fn() để tạo một "hàm giả" (mock function) và kiểm tra xem nó có được gọi hay không.

Component ButtonWrapper.js:

JavaScript
function ButtonWrapper({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>;
}

Test ButtonWrapper.test.js:

JavaScript
test('calls the onClick prop when clicked', () => {
  const handleClick = jest.fn(); // Tạo một mock function
  render(<ButtonWrapper onClick={handleClick}>Click Me</ButtonWrapper>);

  const button = screen.getByText(/click me/i);
  fireEvent.click(button);

  expect(handleClick).toHaveBeenCalledTimes(1); // Khẳng định hàm đã được gọi 1 lần
});

3. Test Component bất đồng bộ (Async)

Khi component cần fetch dữ liệu từ API, ta cần test trạng thái "loading" và trạng thái "success".

  • Sử dụng các truy vấn findBy* của RTL (ví dụ: screen.findByText(...)), chúng sẽ chờ một khoảng thời gian để element xuất hiện.

  • Dùng async/await trong hàm test.

Test:

JavaScript
import { rest } from 'msw';
import { setupServer } from 'msw/node';
// Giả sử bạn dùng MSW (Mock Service Worker) để mock API

// ... (cấu hình server mock) ...

test('loads and displays user data', async () => {
  render(<UserComponent />);

  // Tìm và chờ element loading biến mất (nếu có)
  // ...

  // Chờ cho đến khi tên người dùng xuất hiện sau khi fetch API thành công
  const userNameElement = await screen.findByText(/John Maverick/i);
  expect(userNameElement).toBeInTheDocument();
});

👍 Mẹo và các phương pháp hay nhất (Best Practices)

  1. Ưu tiên truy vấn theo vai trò (role): Dùng getByRole trước (ví dụ: getByRole('button')), vì nó gần nhất với cách người dùng và công nghệ hỗ trợ (như trình đọc màn hình) tương tác với trang web.

  2. Sử dụng getByText cho nội dung văn bản: Đây là cách người dùng thực sự tìm kiếm thông tin.

  3. Hạn chế getByTestId: Chỉ dùng data-testid khi bạn không thể tìm thấy element bằng các cách khác. Lạm dụng nó sẽ khiến test của bạn xa rời trải nghiệm người dùng.

  4. Viết test độc lập: Mỗi test case (it hoặc test) nên độc lập và không phụ thuộc vào kết quả của test case trước đó.

  5. Mỗi test chỉ nên kiểm tra một thứ: Giúp việc gỡ lỗi khi test thất bại trở nên dễ dàng hơn.

Chúc bạn viết test thành công! 🧪

Comment