Kiểm thử thành phần trong thực tế

Kiểm thử thành phần là một nơi phù hợp để bắt đầu minh hoạ mã kiểm thử thực tế. Kiểm thử thành phần quan trọng hơn kiểm thử đơn vị đơn giản, ít phức tạp hơn kiểm thử toàn diện và minh hoạ cách tương tác với DOM. Về mặt triết học, việc sử dụng React giúp các nhà phát triển web dễ dàng xem trang web hoặc ứng dụng web là các thành phần.

Vì vậy, việc kiểm thử từng thành phần, bất kể chúng phức tạp đến mức nào, là một cách hay để bắt đầu suy nghĩ về việc kiểm thử một ứng dụng mới hoặc hiện có.

Trang này hướng dẫn cách kiểm thử một thành phần nhỏ có các phần phụ thuộc bên ngoài phức tạp. Bạn có thể dễ dàng kiểm thử một thành phần không tương tác với bất kỳ mã nào khác, chẳng hạn như bằng cách nhấp vào một nút và xác nhận rằng một con số tăng lên. Trong thực tế, có rất ít mã như vậy và mã kiểm thử không có tương tác có thể có giá trị bị hạn chế.

Thành phần đang được kiểm thử

Chúng ta sử dụng Vitest và môi trường JSDOM của nó để kiểm thử một thành phần React. Điều này cho phép chúng ta chạy kiểm thử nhanh bằng cách sử dụng Node trên dòng lệnh trong khi mô phỏng trình duyệt.

Danh sách tên có nút Chọn bên cạnh mỗi tên.
Một thành phần React nhỏ hiển thị danh sách người dùng trong mạng.

Thành phần React có tên UserList này sẽ tìm nạp danh sách người dùng từ mạng và cho phép bạn chọn một trong số họ. Danh sách người dùng được lấy bằng cách sử dụng fetch bên trong useEffect và trình xử lý lựa chọn được truyền vào bằng Context. Đây là mã của lớp này:

import React, { useEffect, useState, useContext } from 'react';
import { UserContext } from './UserContext.tsx';
import { UserRow } from './UserRow.tsx';

export function UserList({ count = 4 }: { count?: number }) {
  const [users, setUsers] = useState<any[]>([]);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users?_limit=' + count)
      .then((response) => response.json())
      .then((json) => setUsers(json));
  }, [count]);

  const c = useContext(UserContext);
  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            <button onClick={() => c.userChosen(u.id)}>Choose</button>{' '}
            <UserRow u={u} />
          </li>
        ))}
      </ul>
    </div>
  );
}

Ví dụ này không minh hoạ các phương pháp hay nhất của React (ví dụ: ví dụ này sử dụng fetch bên trong useEffect), nhưng cơ sở mã của bạn có thể chứa nhiều trường hợp như vậy. Quan trọng hơn, những trường hợp này có thể khó kiểm thử ngay từ đầu. Một phần sau của khoá học này sẽ thảo luận chi tiết về cách viết mã có thể kiểm thử.

Sau đây là những điều chúng ta sẽ kiểm thử trong ví dụ này:

  • Kiểm tra để đảm bảo rằng một số DOM chính xác được tạo để phản hồi dữ liệu từ mạng.
  • Xác nhận rằng việc nhấp vào một người dùng sẽ kích hoạt lệnh gọi lại.

Mỗi thành phần đều khác nhau. Điều gì khiến việc kiểm thử trường hợp này trở nên thú vị?

  • Thư viện này sử dụng fetch toàn cục để yêu cầu dữ liệu thực tế từ mạng, dữ liệu này có thể không ổn định hoặc bị chậm khi kiểm thử.
  • Thao tác này sẽ nhập một lớp khác là UserRow mà chúng ta có thể không muốn ngầm kiểm thử.
  • Phương thức này sử dụng một Context (không thuộc cụ thể của mã đang được kiểm thử) và thường do một thành phần mẹ cung cấp.

Viết một chương trình kiểm thử nhanh để bắt đầu

Chúng ta có thể nhanh chóng kiểm thử một số thông tin cơ bản về thành phần này. Xin lưu ý rằng ví dụ này không hữu ích lắm. Tuy nhiên, bạn nên thiết lập mã nguyên mẫu trong tệp ngang hàng có tên là UserList.test.tsx (hãy nhớ các trình chạy kiểm thử như Vitest, theo mặc định, chạy các tệp kết thúc bằng .test.js hoặc tương tự, bao gồm cả .tsx):

import { vi, test, assert, afterAll } from 'vitest';
import { render } from '@testing-library/react';
import { UserList } from './UserList.tsx';
import React, { ContextType } from 'react';

test('render', async () => {
  const c = render(<UserList />);

  const headingNode = await c.findAllByText(/Users);
  assert.isNotNull(headingNode);
});

Kiểm thử này khẳng định rằng khi thành phần hiển thị, thành phần đó sẽ chứa văn bản "Người dùng". Thành phần này hoạt động mặc dù thành phần này có tác dụng phụ là gửi fetch đến mạng. fetch vẫn đang diễn ra khi kết thúc kiểm thử, không có điểm cuối nào được đặt. Chúng tôi không thể xác nhận rằng có bất kỳ thông tin người dùng nào đang hiển thị khi kiểm thử kết thúc, ít nhất là không phải chờ hết thời gian chờ.

Mô phỏng fetch()

Mô phỏng là hành động thay thế một hàm hoặc lớp thực bằng một đối tượng nào đó nằm trong quyền kiểm soát của bạn cho một kiểm thử. Đây là phương pháp phổ biến trong hầu hết các loại kiểm thử, ngoại trừ các kiểm thử đơn vị đơn giản nhất. Bạn có thể tìm hiểu thêm về vấn đề này trong phần Xác nhận và các dữ liệu gốc khác.

Bạn có thể mô phỏng fetch() cho quy trình kiểm thử để kiểm thử hoàn tất nhanh chóng và trả về dữ liệu mà bạn mong đợi, chứ không phải dữ liệu "thực tế" hoặc chưa biết. fetch là một mảng toàn cục, nghĩa là chúng ta không cần import hoặc require vào mã của mình.

Trong quá trình vitest, bạn có thể mô phỏng toàn cục bằng cách gọi vi.stubGlobal với một đối tượng đặc biệt do vi.fn() trả về. Thao tác này sẽ tạo một bản mô phỏng mà chúng ta có thể sửa đổi sau. Các phương thức này sẽ được kiểm tra chi tiết hơn trong phần sau của khoá học này, nhưng bạn có thể xem các phương thức này trong thực tế trong mã sau:

test('render', async () => {
  const fetchMock = vi.fn();
  fetchMock.mockReturnValue(
    Promise.resolve({
      json: () => Promise.resolve([{ name: 'Sam', id: 'sam' }]),
    }),
  );
  vi.stubGlobal('fetch', fetchMock);

  const c = render(<UserList />);

  const headingNode = await c.queryByText(/Users);
  assert.isNotNull(headingNode);

  await waitFor(async () => {
    const samNode = await c.queryByText(/Sam);
    assert.isNotNull(samNode);
  });
});

afterAll(() => {
  vi.unstubAllGlobals();
});

Mã này thêm một bản mô phỏng, mô tả phiên bản giả mạo của lệnh tìm nạp mạng Response, sau đó chờ lệnh tìm nạp đó xuất hiện. Nếu văn bản không xuất hiện, bạn có thể kiểm tra điều này bằng cách thay đổi truy vấn trong queryByText thành một tên mới, thì kiểm thử sẽ không thành công.

Ví dụ này sử dụng trình trợ giúp mô phỏng tích hợp sẵn của Vitest, nhưng các khung kiểm thử khác cũng có phương pháp tương tự để mô phỏng. Vitest có một điểm độc đáo là bạn phải gọi vi.unstubAllGlobals() sau tất cả các bài kiểm thử hoặc đặt một tuỳ chọn toàn cục tương đương. Nếu không "huỷ" công việc của chúng ta, mô phỏng fetch có thể ảnh hưởng đến các kiểm thử khác và mọi yêu cầu sẽ được phản hồi bằng một đống JSON kỳ lạ.

Nhập dữ liệu mô phỏng

Bạn có thể nhận thấy rằng thành phần UserList của chúng ta tự nhập một thành phần có tên là UserRow. Mặc dù chúng tôi chưa đưa mã vào, nhưng bạn có thể thấy rằng mã này hiển thị tên người dùng: kiểm thử trước đó kiểm tra "Sam" và tên này không được hiển thị trực tiếp bên trong UserList, vì vậy, tên này phải đến từ UserRow.

Sơ đồ quy trình về cách tên người dùng di chuyển qua thành phần của chúng ta.
UserListTest không có chế độ hiển thị UserRow.

Tuy nhiên, UserRow có thể là một thành phần phức tạp – thành phần này có thể tìm nạp thêm dữ liệu người dùng hoặc có những tác dụng phụ không liên quan đến kiểm thử của chúng ta. Việc loại bỏ tính biến thiên đó sẽ giúp các bài kiểm thử của bạn hữu ích hơn, đặc biệt là khi các thành phần bạn muốn kiểm thử trở nên phức tạp hơn và liên kết chặt chẽ hơn với các phần phụ thuộc của chúng.

May mắn thay, bạn có thể sử dụng Vitest để mô phỏng một số lệnh nhập nhất định, ngay cả khi quy trình kiểm thử của bạn không trực tiếp sử dụng các lệnh nhập đó, để mọi mã sử dụng các lệnh nhập đó đều được cung cấp một phiên bản đơn giản hoặc đã biết:

vi.mock('./UserRow.tsx', () => {
  return {
    UserRow(arg) {
      return <>{arg.u.name}</>;
    },
  }
});

test('render', async () => {
  // ...
});

Giống như việc mô phỏng fetch toàn cục, đây là một công cụ mạnh mẽ, nhưng có thể trở nên không bền vững nếu mã của bạn có nhiều phần phụ thuộc. Xin nhắc lại, cách khắc phục tốt nhất là viết mã có thể kiểm thử.

Nhấp và cung cấp bối cảnh

React và các thư viện khác chẳng hạn như Lit có một khái niệm gọi là Context. Mã mẫu bao gồm UserContext, gọi phương thức nếu người dùng được chọn. Phương thức này thường được xem là một giải pháp thay thế cho "đục lỗ đối tượng", trong đó lệnh gọi lại được chuyển trực tiếp đến UserList.

Bộ kiểm thử của chúng tôi chưa cung cấp UserContext. Việc thêm thao tác nhấp vào kiểm thử React mà không có thao tác này, trong trường hợp xấu nhất, có thể làm hỏng kiểm thử. Tốt nhất là nếu một thực thể mặc định được cung cấp ở nơi khác, thì thực thể đó có thể khiến một số hành vi nằm ngoài tầm kiểm soát của chúng ta (tương tự như UserRow không xác định ở trên).

  const c = render(<UserList />);
  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();

Thay vào đó, khi kết xuất thành phần, bạn có thể cung cấp Context của riêng mình. Ví dụ này sử dụng một thực thể của vi.fn(), một Hàm mô phỏng Vitest, có thể được dùng để kiểm tra xem một lệnh gọi đã được thực hiện hay chưa và lệnh gọi đó đã sử dụng đối số nào.

Trong trường hợp của chúng ta, mã này tương tác với fetch được mô phỏng trong ví dụ trước và kiểm thử có thể xác nhận rằng mã nhận dạng được truyền qua là sam:

  const userChosenFn = vi.fn();
  const ucForTest: ContextType<typeof UserContext> = { userChosen: userChosenFn as any };
  const c = render(
    <UserContext.Provider value={ucForTest}>
      <UserList />
    </UserContext.Provider>,
  );

  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();
  assert.deepStrictEqual(userChosenFn.mock.calls, [['sam']]);

Đây là một mẫu đơn giản nhưng mạnh mẽ, cho phép bạn xoá các phần phụ thuộc không liên quan khỏi thành phần cốt lõi mà bạn đang cố gắng kiểm thử.

Tóm tắt

Ví dụ này minh hoạ cách tạo kiểm thử thành phần để kiểm thử và bảo vệ một thành phần React khó kiểm thử. Thử nghiệm này tập trung vào việc đảm bảo rằng thành phần này tương tác chính xác với các phần phụ thuộc của nó: fetch chung, một thành phần phụ đã nhập và một Context.

Kiểm tra kiến thức

Bạn đã sử dụng phương pháp nào để kiểm thử thành phần React?

Tạo mã giả lập toàn cục
Kiểm tra để đảm bảo số đã tăng
Mô phỏng các phần phụ thuộc phức tạp bằng các phần phụ thuộc đơn giản để kiểm thử
Chèn phần phụ thuộc bằng Ngữ cảnh