React 20's Compiler and concurrent features have changed the behavioral patterns of components, so testing strategies need to be updated accordingly. By 2025, React testing has converged on a mature set of patterns: Vitest as the test runner, Testing Library for component tests, Playwright for E2E, and MSW for network request interception.
Vitest Configuration and React 20 Adaptation
javascript
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
react({
// Compiler needs to be enabled in the test environment too
babel: {
plugins: [["babel-plugin-react-compiler", { target: "19" }]],
},
}),
],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
// Concurrent tests, leverage multiple cores
pool: "threads",
poolOptions: {
threads: { maxThreads: 4 },
},
// React 20's Suspense requires special handling
environmentOptions: {
jsdom: {
resources: "usable",
},
},
},
});
typescript
// src/test/setup.ts
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach, vi } from "vitest";
afterEach(() => {
cleanup(); // React 20 requires explicit cleanup
});
// Mock the async behavior of useTransition
vi.mock("react", async () => {
const actual = await vi.importActual("react");
return {
...actual,
// In the test environment, transitions execute synchronously
useTransition: () => [false, (cb: () => void) => cb()],
};
});
Component Tests: Behavior First
2025 component tests emphasize testing user behavior, not component implementation. React 20's useActionState and useField make this philosophy easier to put into practice.
typescript
// components/LoginForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { describe, it, expect } from 'vitest';
import LoginForm from './LoginForm';
const server = setupServer(
http.post('/api/login', async ({ request }) => {
const body = await request.json();
if (body.email === 'test@example.com' && body.password === 'password123') {
return HttpResponse.json({ token: 'abc', user: { name: '张三' } });
}
return HttpResponse.json(
{ error: '邮箱或密码错误' },
{ status: 401 }
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('LoginForm', () => {
it('redirects to home on successful login', async () => {
const user = userEvent.setup();
const onSuccess = vi.fn();
render(<LoginForm onSuccess={onSuccess} />);
await user.type(screen.getByLabelText('邮箱'), 'test@example.com');
await user.type(screen.getByLabelText('密码'), 'password123');
await user.click(screen.getByRole('button', { name: '登录' }));
// Wait for async operation to complete
await waitFor(() => {
expect(onSuccess).toHaveBeenCalledWith(
expect.objectContaining({ user: { name: '张三' } })
);
});
});
it('shows error message when form validation fails', async () => {
const user = userEvent.setup();
render(<LoginForm />);
// Type an invalid email only
await user.type(screen.getByLabelText('邮箱'), 'invalid');
await user.tab(); // trigger blur event
expect(await screen.findByText('请输入有效的邮箱')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '登录' })).toBeDisabled();
});
});
Testing Strategy for Suspense Components
React 20 components may suspend during rendering, and tests need to handle this behavior:
typescript
// components/UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { Suspense } from 'react';
import { describe, it, expect } from 'vitest';
import UserProfile from './UserProfile';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
function renderWithProviders(ui: React.ReactElement) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
});
return render(
<QueryClientProvider client={queryClient}>
<Suspense fallback={<div data-testid="loading">加载中...</div>}>
{ui}
</Suspense>
</QueryClientProvider>
);
}
describe('UserProfile', () => {
it('shows user info after loading completes', async () => {
// MSW mock API response
server.use(
http.get('/api/users/1', () => {
return HttpResponse.json({
id: '1',
name: '李四',
bio: '前端工程师',
});
})
);
renderWithProviders(<UserProfile userId="1" />);
// First shows loading
expect(screen.getByTestId('loading')).toBeInTheDocument();
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('李四')).toBeInTheDocument();
});
expect(screen.getByText('前端工程师')).toBeInTheDocument();
});
});
E2E Testing: Playwright Best Practices
typescript
// e2e/checkout.spec.ts
import { test, expect } from "@playwright/test";
test.describe("购物流程", () => {
test("添加商品到购物车并结算", async ({ page }) => {
await page.goto("/products");
// 选择商品
await page.getByRole("button", { name: "添加到购物车" }).first().click();
// 验证购物车图标更新
await expect(page.getByTestId("cart-count")).toHaveText("1");
// 进入购物车
await page.getByRole("link", { name: "购物车" }).click();
// 填写收货地址(使用 useField 的表单)
await page.getByPlaceholder("收货地址").fill("北京市朝阳区");
await page.getByPlaceholder("手机号").fill("13800138000");
// 提交订单
await page.getByRole("button", { name: "提交订单" }).click();
// 等待 Server Action 完成
await expect(page.getByText("订单提交成功")).toBeVisible({
timeout: 10000,
});
});
test("网络异常时显示错误提示", async ({ page }) => {
// 模拟网络失败
await page.route("/api/orders", (route) => route.abort());
await page.goto("/checkout");
await page.getByRole("button", { name: "提交订单" }).click();
await expect(page.getByText("网络异常,请重试")).toBeVisible();
});
});
Summary
- Vitest has fully replaced Jest; remember to enable React Compiler and Suspense support during configuration
- Component tests are user-behavior-driven; MSW intercepts network requests to avoid backend dependencies
- Suspense components need to be wrapped in a
QueryClientProviderand aSuspenseboundary for testing - Playwright is the first choice for E2E testing; route interception can simulate various network scenarios
- Recommended testing pyramid ratio: 60% unit tests, 30% component tests, 10% E2E tests