•9 min read

Kiểm thử React hiện đại với Vitest và React Testing Library

Kiểm thử React hiện đại với Vitest và React Testing Library

Việc kiểm thử ứng dụng React đã phát triển đáng kể qua nhiều năm. Trong một thời gian dài, Jest là ông vua không thể tranh cãi của hệ sinh thái kiểm thử JavaScript. Tuy nhiên, khi ngành công nghiệp chuyển sang ES Modules (ESM) và các công cụ build hiện đại như Vite, Jest bắt đầu bộc lộ những hạn chế của mình—đặc biệt là về tốc độ thực thi và khả năng tương thích ESM. Vitest xuất hiện, một framework kiểm thử cực nhanh, tích hợp sâu với Vite, đang nhanh chóng trở thành tiêu chuẩn mới cho các dự án React.

Trong hướng dẫn toàn diện này, chúng ta sẽ đi sâu vào việc di chuyển từ Jest sang Vitest, tích hợp nó với React Testing Library (RTL), và tận dụng các mẫu kiểm thử hiện đại vượt trội hơn các hướng dẫn truyền thống.

Audio Briefing
0:00 / 0:00

Tại sao phải di chuyển từ Jest sang Vitest?

Trước khi đi sâu vào code, điều quan trọng là phải hiểu tại sao sự thay đổi này lại diễn ra. Mặc dù các hướng dẫn toàn diện (như của Robin Wieruch) trước đây đã ủng hộ Jest, nhưng bối cảnh frontend hiện đại đòi hỏi chu kỳ lặp lại nhanh hơn.

  1. Hỗ trợ ESM gốc: Jest trước đây đã gặp khó khăn với ESM, thường yêu cầu cấu hình Babel phức tạp hoặc các cờ thử nghiệm. Vitest hỗ trợ ESM ngay lập tức.
  2. Tích hợp Vite: Nếu ứng dụng React của bạn đã sử dụng Vite, Vitest chia sẻ cùng cấu hình (vite.config.ts), plugin và pipeline chuyển đổi. Điều này loại bỏ sự khác biệt đáng sợ "hoạt động trong dev, lỗi trong test".
  3. Tốc độ: Bằng cách tận dụng ESBuild để chuyển đổi, Vitest thực thi các bài kiểm thử nhanh hơn đáng kể so với Jest, đặc biệt là trong các codebase lớn.
  4. Chế độ Watch: Chế độ watch thông minh của Vitest (được cung cấp bởi cơ chế HMR của Vite) ngay lập tức chạy lại chỉ những bài kiểm thử bị ảnh hưởng bởi các thay đổi tệp.
  5. Khả năng tương thích API: Vitest cung cấp API tương thích với Jest, giúp việc di chuyển trở nên đơn giản đáng ngạc nhiên. describe, it, expect và vi (tương đương với jest của Vitest) hầu hết là các thay thế trực tiếp.
Advertisement

Bước 1: Cài đặt và Cấu hình

Hãy bắt đầu bằng cách loại bỏ Jest và cài đặt Vitest cùng với React Testing Library.

npm uninstall jest @types/jest ts-jest @testing-library/jest-dom
npm install -D vitest @vitest/ui @testing-library/react @testing-library/jest-dom jsdom @testing-library/user-event

Tiếp theo, cấu hình Vitest. Nếu bạn đang sử dụng Vite, chỉ cần thêm thuộc tính test vào vite.config.ts của bạn:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true, // Enables 'describe', 'it', 'expect' globally like Jest
    environment: 'jsdom', // Simulates a browser environment for React components
    setupFiles: './src/setupTests.ts', // Path to your setup file
    css: true, // Process CSS imports
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

Thiết lập Môi trường

Tạo src/setupTests.ts để mở rộng expect của Vitest với các matcher DOM từ @testing-library/jest-dom:

// src/setupTests.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';

// Automatically unmount and cleanup DOM after the test is finished.
afterEach(() => {
  cleanup();
});

// Mocking window.matchMedia if needed for UI libraries
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: vi.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
  })),
});

Lưu ý chúng ta import @testing-library/jest-dom/vitest. Đây là một bổ sung mới hơn cung cấp các kiểu Vitest gốc, tránh xung đột namespace với Jest.

Bước 2: Quá trình Di chuyển (Jest sang Vitest)

Di chuyển các tệp kiểm thử của bạn thường là một thao tác tìm kiếm và thay thế:

  1. Globals: Nếu bạn đặt globals: true trong cấu hình của mình, bạn không cần import describe, it hoặc expect từ vitest.
  2. Mocking: Thay thế jest.fn() bằng vi.fn(), jest.spyOn() bằng vi.spyOn(), và jest.mock() bằng vi.mock().
  3. Timers: Thay thế jest.useFakeTimers() bằng vi.useFakeTimers().

Ví dụ Mocking Nâng cao

Hãy xem cách mocking một module thay đổi.

Jest:

jest.mock('axios', () => ({
  default: { get: jest.fn().mockResolvedValue({ data: {} }) }
}));

Vitest:

import { vi } from 'vitest';
import axios from 'axios';

vi.mock('axios', () => ({
  default: { get: vi.fn().mockResolvedValue({ data: {} }) }
}));

Mặc dù có cấu trúc giống hệt nhau, cơ chế hoisting của Vitest hơi khác một chút. Vitest hoist vi.mock lên đầu tệp, giống như Jest, nhưng nó tích hợp liền mạch hơn với ESM, giảm các trường hợp ngoại lệ khi các module được mock đánh giá trước khi mock được áp dụng.

Bước 3: Kiểm thử Component Hiện đại với React Testing Library

React Testing Library (RTL) khuyến khích kiểm thử các component từ góc độ người dùng thay vì kiểm thử các chi tiết triển khai. Hãy xây dựng một component bất đồng bộ phức tạp và kiểm thử nó một cách đúng đắn.

// src/components/UserProfile.tsx
import React, { useState, useEffect } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
}

export const UserProfile = ({ userId }: { userId: string }) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('Failed to fetch user');
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error');
      } finally {
        setLoading(false);
      }
    };
    fetchUser();
  }, [userId]);

  if (loading) return <div role="status">Loading...</div>;
  if (error) return <div role="alert">{error}</div>;
  if (!user) return <div>No user found</div>;

  return (
    <article>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <button onClick={() => console.log('Edit clicked')}>Edit Profile</button>
    </article>
  );
};

Viết bài kiểm thử

Đây là cách bạn viết một bộ kiểm thử hiện đại, bền vững cho component này bằng Vitest và RTL, tránh các anti-pattern phổ biến như sử dụng waitFor một cách không cần thiết.

// src/components/UserProfile.test.tsx
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from './UserProfile';
import { vi, describe, it, expect, beforeEach } from 'vitest';

// Mock the global fetch
global.fetch = vi.fn();

describe('UserProfile Component', () => {
  const mockUser = { id: '1', name: 'Jane Doe', email: 'jane@example.com' };

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('renders loading state initially, then displays user data', async () => {
    (global.fetch as any).mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser,
    });

    render(<UserProfile userId="1" />);

    // 1. Assert loading state
    expect(screen.getByRole('status')).toHaveTextContent('Loading...');

    // 2. Wait for loading to finish (Modern pattern instead of waitFor)
    await waitForElementToBeRemoved(() => screen.queryByRole('status'));

    // 3. Assert user data
    expect(screen.getByRole('heading', { name: 'Jane Doe' })).toBeInTheDocument();
    expect(screen.getByText('jane@example.com')).toBeInTheDocument();
  });

  it('renders error state on API failure', async () => {
    (global.fetch as any).mockResolvedValueOnce({
      ok: false,
    });

    render(<UserProfile userId="2" />);

    // Wait for the alert role to appear
    const alertElement = await screen.findByRole('alert');
    expect(alertElement).toHaveTextContent('Failed to fetch user');
  });

  it('simulates user interaction correctly', async () => {
    (global.fetch as any).mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser,
    });
    const consoleSpy = vi.spyOn(console, 'log');

    render(<UserProfile userId="1" />);
    
    // Wait for data to load using findByRole
    const editButton = await screen.findByRole('button', { name: /edit profile/i });

    // Use userEvent instead of fireEvent for more realistic interactions
    const user = userEvent.setup();
    await user.click(editButton);

    expect(consoleSpy).toHaveBeenCalledWith('Edit clicked');
  });
});

Những điểm chính rút ra từ bộ kiểm thử:

  1. userEvent thay vì fireEvent: Chúng tôi sử dụng @testing-library/user-event. Nó mô phỏng các sự kiện DOM đầy đủ (như focus bàn phím, bubbling, v.v.), làm cho các bài kiểm thử gần hơn đáng kể với cách người dùng thực tương tác với trình duyệt.
  2. findBy* thay vì waitFor: Thay vì bọc một getBy* trong một khối waitFor, RTL hiện đại khuyến khích sử dụng await screen.findBy*(). Nó sạch hơn và xử lý việc thăm dò nội bộ.
  3. waitForElementToBeRemoved: Đây là cách sạch nhất để khẳng định rằng một skeleton loading hoặc spinner đã biến mất trước khi tiếp tục khẳng định nội dung được render.
Advertisement

Tích hợp liên tục và Độ bao phủ

Vitest sử dụng c8 (thông qua v8) hoặc istanbul để đo độ bao phủ mã. Để bật độ bao phủ, hãy chạy:

npx vitest run --coverage

Bạn có thể cần cài đặt nhà cung cấp: npm i -D @vitest/coverage-v8. Kết quả được tạo ra ngay lập tức, và vì nó chạy trên ESBuild, nó cực kỳ nhanh trong các pipeline CI.

Kết luận

Di chuyển từ Jest sang Vitest không chỉ là theo kịp xu hướng; đó là một nâng cấp thực tế cho trải nghiệm của nhà phát triển. Bạn có được hỗ trợ ESM gốc, cấu hình chia sẻ với Vite và tốc độ thực thi có thể giảm một nửa thời gian CI.

Khi kết hợp với cách tiếp cận triết lý của React Testing Library—kiểm thử hành vi thay vì chi tiết triển khai—bộ kiểm thử của bạn trở thành một mạng lưới an toàn mạnh mẽ thay vì một gánh nặng bảo trì dễ vỡ. Hãy đón nhận Vitest, sử dụng userEvent, và ngừng kiểm thử trạng thái nội bộ của các component. Chúc bạn kiểm thử vui vẻ!

Bạn cũng có thể thích

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement
Sự thay đổi mô hình của React Server Components
react

Sự thay đổi mô hình của React Server Components

Khám phá cách React Server Components (RSC) thay đổi cơ bản cách chúng ta xây dựng ứng dụng React, mang lại kích thước bundle nhỏ hơn, đơn giản hóa việc tìm nạp dữ liệu và cải thiện hiệu suất.

Read more