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

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

Vì vậy, việc kiểm thử các thành phần riêng lẻ, bất kể mức độ phức tạp của các thành phần đó như thế nào, là một cách hay để bắt đầu cân nhắc việc kiểm thử một ứng dụng mới hoặc ứng dụng hiện có.

Trang này hướng dẫn cách kiểm thử một thành phần nhỏ với 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ế, rất ít mã như vậy và mã kiểm thử không có lượt tương tác có thể có giá trị giới hạn.

(Phần này không phải là hướng dẫn đầy đủ. Phần sau, Kiểm thử tự động trong thực tế, sẽ hướng dẫn bạn cách kiểm thử trang web thực tế bằng mã mẫu mà bạn có thể dùng làm hướng dẫn). Tuy nhiên, trang này vẫn sẽ đề cập đến một số ví dụ về kiểm thử thành phần thực tế.)

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

Chúng ta sẽ sử dụng Vitest và môi trường JSDOM của Vitest để kiểm thử một thành phần React. Điều này cho phép chúng ta nhanh chóng chạy kiểm thử bằng Nút trên dòng lệnh trong khi mô phỏng một 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 trên mạng.

Thành phần React này có tên là UserList 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ố đó. Bạn có thể lấy danh sách người dùng bằng cách sử dụng fetch bên trong useEffect, còn trình xử lý lựa chọn được Context truyền vào. Dưới đây là mã:

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 về React (ví dụ: 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ể có vẻ khó kiểm tra ngay từ đầu. Một phần tiếp theo 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 nội dung mà chúng tôi sẽ thử nghiệm trong ví dụ này:

  • Kiểm tra để đảm bảo rằng một số DOM chính xác được tạo theo 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 thử nghiệm định dạng này trở nên thú vị?

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

Viết kiểm thử nhanh để bắt đầu

Chúng ta có thể nhanh chóng kiểm thử một số thông tin rất cơ bản về thành phần này. Để cho rõ ràng, ví dụ này không hữu ích lắm! Tuy nhiên, sẽ rất hữu ích nếu bạn thiết lập bản mẫu trong tệp ngang hàng có tên UserList.test.tsx (hãy nhớ rằng các trình chạy kiểm thử như Vitest, theo mặc định, sẽ 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 xác nhận rằng khi thành phần kết xuất, thành phần đó có chứa văn bản "Users" (Người dùng). Tính năng 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 ở cuối quá trình kiểm thử, chưa 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 nào về người dùng đang hiển thị khi quá trình kiểm thử kết thúc, ít nhất là không cần 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 nội dung thuộc quyền kiểm soát của bạn để 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. Vấn đề này sẽ được đề cập thêm 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ử để nhanh chóng hoàn tất và trả về dữ liệu mà bạn mong đợi, chứ không phải dữ liệu "thực tế" hay không xác định. fetch là một toàn cầu, có nghĩa là chúng ta không phải import hoặc require nó vào mã của mình.

Trong vi kiểm thử, bạn có thể mô phỏng một tập hợp 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 tìm hiểu chi tiết hơn trong phần sau của khoá học này. Tuy nhiên, 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 mô hình mô phỏng, mô tả một phiên bản "giả" của mạng tìm nạp Response, sau đó chờ mã này 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 – bài kiểm thử sẽ không thành công.

Ví dụ này sử dụng các 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 khác biệt ở chỗ bạn phải gọi vi.unstubAllGlobals() sau tất cả các hoạt động 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 đống JSON lẻ của chúng ta!

Nội dung nhập mô phỏng

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

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

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

May mắn là 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ử không trực tiếp sử dụng các lệnh nhập đó, để bất kỳ mã nào 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ư 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 cho điều đó là viết mã có thể kiểm thử.

Nhấp vào để 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 tên là Context. Mã mẫu bao gồm một UserContext gọi phương thức nếu người dùng được chọn. Đây thường được xem là giải pháp thay thế cho "prop drilling", trong đó lệnh gọi lại được chuyển trực tiếp đến UserList.

Phần khai thác kiểm thử mà chúng ta đã viết chưa cung cấp UserContext. Khi bạn thêm một thao tác nhấp vào kiểm thử React mà không có hành động này, thì điều này ít nhất sẽ khiến bài kiểm thử gặp sự cố hoặc tốt nhất là nếu một thực thể mặc định được cung cấp ở nơi khác, sẽ gây ra 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 thực thể của vi.fn(), một Hàm mô phỏng Vitest, có thể được dùng sau khi thực tế để kiểm tra xem lệnh gọi đã được thực hiện hay chưa và lệnh gọi đó được thực hiện bằng đối số nào. Trong trường hợp của chúng ta, hàm này tương tác với fetch mô phỏng trong ví dụ trước và quy trình 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ẽ có thể 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ử.

Trong phần tóm tắt

Đây là một ví dụ nhanh chóng và đơn giản minh hoạ cách tạo một chương trình 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ử, tập trung vào việc đảm bảo thành phần đó tương tác chính xác với các phần phụ thuộc (thành phần toàn cục fetch, một thành phần phụ được nhập và một Context).

Kiểm tra kiến thức

Những phương pháp nào được dùng để kiểm thử thành phần React?

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 Context
Lấy tập lệnh toàn cục
Kiểm tra để đảm bảo rằng một số tăng lên