Tiếng Việt

Làm chủ React Testing Library (RTL) với hướng dẫn đầy đủ này. Học cách viết các bài test hiệu quả, dễ bảo trì và tập trung vào người dùng cho ứng dụng React của bạn, tập trung vào các phương pháp hay nhất và ví dụ thực tế.

React Testing Library: Hướng dẫn Toàn diện

Trong bối cảnh phát triển web có nhịp độ nhanh ngày nay, việc đảm bảo chất lượng và độ tin cậy của các ứng dụng React của bạn là vô cùng quan trọng. React Testing Library (RTL) đã nổi lên như một giải pháp phổ biến và hiệu quả để viết các bài test tập trung vào góc nhìn của người dùng. Hướng dẫn này cung cấp một cái nhìn tổng quan hoàn chỉnh về RTL, bao gồm mọi thứ từ các khái niệm cơ bản đến các kỹ thuật nâng cao, giúp bạn xây dựng các ứng dụng React mạnh mẽ và dễ bảo trì.

Tại sao nên chọn React Testing Library?

Các phương pháp kiểm thử truyền thống thường dựa vào chi tiết triển khai, làm cho các bài test trở nên mong manh và dễ bị hỏng khi có những thay đổi nhỏ về mã nguồn. Ngược lại, RTL khuyến khích bạn kiểm thử các component của mình như cách người dùng sẽ tương tác với chúng, tập trung vào những gì người dùng nhìn thấy và trải nghiệm. Cách tiếp cận này mang lại một số lợi thế chính:

Thiết lập Môi trường Kiểm thử của bạn

Trước khi có thể bắt đầu sử dụng RTL, bạn cần thiết lập môi trường kiểm thử của mình. Điều này thường bao gồm việc cài đặt các dependency cần thiết và cấu hình framework kiểm thử của bạn.

Điều kiện tiên quyết

Cài đặt

Cài đặt các gói sau bằng npm hoặc yarn:

npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Hoặc, sử dụng yarn:

yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Giải thích các gói:

Cấu hình

Tạo một tệp `babel.config.js` ở thư mục gốc của dự án với nội dung sau:

module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react'],
};

Cập nhật tệp `package.json` của bạn để bao gồm một script test:

{
  "scripts": {
    "test": "jest"
  }
}

Tạo một tệp `jest.config.js` ở thư mục gốc của dự án để cấu hình Jest. Một cấu hình tối thiểu có thể trông như thế này:

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['/src/setupTests.js'],
};

Tạo một tệp `src/setupTests.js` với nội dung sau. Điều này đảm bảo rằng các matcher Jest DOM có sẵn trong tất cả các bài test của bạn:

import '@testing-library/jest-dom/extend-expect';

Viết bài Test đầu tiên của bạn

Hãy bắt đầu với một ví dụ đơn giản. Giả sử bạn có một component React hiển thị một thông điệp chào mừng:

// src/components/Greeting.js
import React from 'react';

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

export default Greeting;

Bây giờ, hãy viết một bài test cho component này:

// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';

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

Giải thích:

Để chạy bài test, thực thi lệnh sau trong terminal của bạn:

npm test

Nếu mọi thứ được cấu hình chính xác, bài test sẽ thành công.

Các Truy vấn RTL Phổ biến

RTL cung cấp nhiều phương thức truy vấn khác nhau để tìm các phần tử trong DOM. Các truy vấn này được thiết kế để bắt chước cách người dùng tương tác với ứng dụng của bạn.

`getByRole`

Truy vấn này tìm một phần tử theo vai trò ARIA của nó. Đây là một phương pháp hay để sử dụng `getByRole` bất cứ khi nào có thể, vì nó thúc đẩy khả năng tiếp cận và đảm bảo rằng các bài test của bạn có khả năng phục hồi trước những thay đổi trong cấu trúc DOM cơ bản.

<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();

`getByLabelText`

Truy vấn này tìm một phần tử theo văn bản của nhãn liên quan đến nó. Nó hữu ích cho việc kiểm thử các phần tử biểu mẫu.

<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();

`getByPlaceholderText`

Truy vấn này tìm một phần tử theo văn bản giữ chỗ của nó.

<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();

`getByAltText`

Truy vấn này tìm một phần tử hình ảnh theo văn bản thay thế (alt text) của nó. Điều quan trọng là phải cung cấp văn bản thay thế có ý nghĩa cho tất cả các hình ảnh để đảm bảo khả năng tiếp cận.

<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();

`getByTitle`

Truy vấn này tìm một phần tử theo thuộc tính title của nó.

<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();

`getByDisplayValue`

Truy vấn này tìm một phần tử theo giá trị hiển thị của nó. Điều này hữu ích cho việc kiểm thử các ô nhập liệu của biểu mẫu với các giá trị được điền sẵn.

<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();

Truy vấn `getAllBy*`

Ngoài các truy vấn `getBy*`, RTL cũng cung cấp các truy vấn `getAllBy*`, trả về một mảng các phần tử khớp. Chúng hữu ích khi bạn cần xác nhận rằng nhiều phần tử có cùng đặc điểm có mặt trong DOM.

<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);

Truy vấn `queryBy*`

Các truy vấn `queryBy*` tương tự như các truy vấn `getBy*`, nhưng chúng trả về `null` nếu không tìm thấy phần tử nào khớp, thay vì ném ra lỗi. Điều này hữu ích khi bạn muốn xác nhận rằng một phần tử *không* có mặt trong DOM.

const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();

Truy vấn `findBy*`

Các truy vấn `findBy*` là các phiên bản bất đồng bộ của các truy vấn `getBy*`. Chúng trả về một Promise sẽ được giải quyết khi tìm thấy phần tử khớp. Chúng hữu ích cho việc kiểm thử các hoạt động bất đồng bộ, chẳng hạn như tìm nạp dữ liệu từ API.

// Mô phỏng việc tìm nạp dữ liệu bất đồng bộ
const fetchData = () => new Promise(resolve => {
  setTimeout(() => resolve('Data Loaded!'), 1000);
});

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
test('loads data asynchronously', async () => {
  render(<MyComponent />);
  const dataElement = await screen.findByText('Data Loaded!');
  expect(dataElement).toBeInTheDocument();
});

Mô phỏng Tương tác Người dùng

RTL cung cấp các API `fireEvent` và `userEvent` để mô phỏng các tương tác của người dùng, chẳng hạn như nhấp vào nút, nhập vào các trường nhập liệu và gửi biểu mẫu.

`fireEvent`

`fireEvent` cho phép bạn kích hoạt các sự kiện DOM một cách lập trình. Đây là một API cấp thấp hơn cho phép bạn kiểm soát chi tiết các sự kiện được kích hoạt.

<button onClick={() => alert('Button clicked!')}>Click me</button>
import { fireEvent } from '@testing-library/react';

test('simulates a button click', () => {
  const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
  render(<button onClick={() => alert('Button clicked!')}>Click me</button>);
  const buttonElement = screen.getByRole('button');
  fireEvent.click(buttonElement);
  expect(alertMock).toHaveBeenCalledTimes(1);
  alertMock.mockRestore();
});

`userEvent`

`userEvent` là một API cấp cao hơn mô phỏng các tương tác của người dùng một cách thực tế hơn. Nó xử lý các chi tiết như quản lý tiêu điểm và thứ tự sự kiện, làm cho các bài test của bạn mạnh mẽ hơn và ít bị mong manh hơn.

<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';

test('simulates typing in an input field', () => {
  const inputElement = screen.getByRole('textbox');
  userEvent.type(inputElement, 'Hello, world!');
  expect(inputElement).toHaveValue('Hello, world!');
});

Kiểm thử Mã Bất đồng bộ

Nhiều ứng dụng React liên quan đến các hoạt động bất đồng bộ, chẳng hạn như tìm nạp dữ liệu từ một API. RTL cung cấp một số công cụ để kiểm thử mã bất đồng bộ.

`waitFor`

`waitFor` cho phép bạn chờ một điều kiện trở thành sự thật trước khi đưa ra một xác nhận. Nó hữu ích cho việc kiểm thử các hoạt động bất đồng bộ mất một khoảng thời gian để hoàn thành.

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    setTimeout(() => {
      setData('Data loaded!');
    }, 1000);
  }, []);

  return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';

test('waits for data to load', async () => {
  render(<MyComponent />);
  await waitFor(() => screen.getByText('Data loaded!'));
  const dataElement = screen.getByText('Data loaded!');
  expect(dataElement).toBeInTheDocument();
});

Truy vấn `findBy*`

Như đã đề cập trước đó, các truy vấn `findBy*` là bất đồng bộ và trả về một Promise sẽ được giải quyết khi tìm thấy phần tử khớp. Chúng hữu ích cho việc kiểm thử các hoạt động bất đồng bộ dẫn đến thay đổi trong DOM.

Kiểm thử Hooks

React Hooks là các hàm có thể tái sử dụng để đóng gói logic có trạng thái. RTL cung cấp tiện ích `renderHook` từ `@testing-library/react-hooks` (đã không còn được dùng nữa thay vào đó là `@testing-library/react` trực tiếp kể từ v17) để kiểm thử các Hook tùy chỉnh một cách riêng biệt.

// src/hooks/useCounter.js
import { useState } from 'react';

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

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  const decrement = () => {
    setCount(prevCount => prevCount - 1);
  };

  return { count, increment, decrement };
}

export default useCounter;
// src/hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';

test('increments the counter', () => {
  const { result } = renderHook(() => useCounter());

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

Giải thích:

Các Kỹ thuật Kiểm thử Nâng cao

Khi bạn đã thành thạo các kiến thức cơ bản về RTL, bạn có thể khám phá các kỹ thuật kiểm thử nâng cao hơn để cải thiện chất lượng và khả năng bảo trì của các bài test của mình.

Mock các Module

Đôi khi, bạn có thể cần phải mock các module hoặc dependency bên ngoài để cô lập các component của mình và kiểm soát hành vi của chúng trong quá trình kiểm thử. Jest cung cấp một API mock mạnh mẽ cho mục đích này.

// src/api/dataService.js
export const fetchData = async () => {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
};
// src/components/MyComponent.js
import React, { useState, useEffect } from 'react';
import { fetchData } from '../api/dataService';

function MyComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
// src/components/MyComponent.test.js
import { render, screen, waitFor } from '@testing-library/react';
import MyComponent from './MyComponent';
import * as dataService from '../api/dataService';

jest.mock('../api/dataService');

test('fetches data from the API', async () => {
  dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' });

  render(<MyComponent />);

  await waitFor(() => screen.getByText('Mocked data!'));

  expect(screen.getByText('Mocked data!')).toBeInTheDocument();
  expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});

Giải thích:

Context Providers

Nếu component của bạn phụ thuộc vào một Context Provider, bạn sẽ cần bọc component của mình trong provider đó trong quá trình kiểm thử. Điều này đảm bảo rằng component có quyền truy cập vào các giá trị context.

// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react';

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
// src/components/MyComponent.js
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';

function MyComponent() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div style={{ backgroundColor: theme === 'light' ? '#fff' : '#000', color: theme === 'light' ? '#000' : '#fff' }}>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}
// src/components/MyComponent.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { ThemeProvider } from '../contexts/ThemeContext';

test('toggles the theme', () => {
  render(
    <ThemeProvider>
      <MyComponent />
    </ThemeProvider>
  );

  const themeParagraph = screen.getByText(/Current theme: light/i);
  const toggleButton = screen.getByRole('button', { name: /Toggle Theme/i });

  expect(themeParagraph).toBeInTheDocument();

  fireEvent.click(toggleButton);

  expect(screen.getByText(/Current theme: dark/i)).toBeInTheDocument();
});

Giải thích:

Kiểm thử với Router

Khi kiểm thử các component sử dụng React Router, bạn sẽ cần cung cấp một context Router giả. Bạn có thể đạt được điều này bằng cách sử dụng component `MemoryRouter` từ `react-router-dom`.

// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';

function MyComponent() {
  return (
    <div>
      <Link to="/about">Go to About Page</Link>
    </div>
  );
}
// src/components/MyComponent.test.js
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import MyComponent from './MyComponent';

test('renders a link to the about page', () => {
  render(
    <MemoryRouter>
      <MyComponent />
    </MemoryRouter>
  );

  const linkElement = screen.getByRole('link', { name: /Go to About Page/i });
  expect(linkElement).toBeInTheDocument();
  expect(linkElement).toHaveAttribute('href', '/about');
});

Giải thích:

Các Phương pháp Tốt nhất để Viết Test Hiệu quả

Dưới đây là một số phương pháp tốt nhất để tuân theo khi viết test với RTL:

Kết luận

React Testing Library là một công cụ mạnh mẽ để viết các bài test hiệu quả, dễ bảo trì và tập trung vào người dùng cho các ứng dụng React của bạn. Bằng cách tuân theo các nguyên tắc và kỹ thuật được nêu trong hướng dẫn này, bạn có thể xây dựng các ứng dụng mạnh mẽ và đáng tin cậy đáp ứng nhu cầu của người dùng. Hãy nhớ tập trung vào việc kiểm thử từ góc nhìn của người dùng, tránh kiểm thử các chi tiết triển khai và viết các bài test rõ ràng và ngắn gọn. Bằng cách áp dụng RTL và các phương pháp tốt nhất, bạn có thể cải thiện đáng kể chất lượng và khả năng bảo trì của các dự án React của mình, bất kể vị trí của bạn hay các yêu cầu cụ thể của khán giả toàn cầu của bạn.