vitest-testing-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Vitest Testing Patterns

Vitest 测试模式

This skill helps you write effective tests using Vitest and React Testing Library following project conventions.
本技能可帮助你遵循项目规范,使用Vitest和React Testing Library编写高效的测试用例。

When to Use

适用场景

USE this skill for:
  • Writing unit tests for utilities and functions
  • Creating component tests with React Testing Library
  • Setting up mocks for API calls, databases, or external services
  • Integration testing patterns
  • Understanding test coverage and CI setup
DO NOT use for:
  • Jest-specific patterns → similar but check Jest docs for differences
  • End-to-end testing → use Playwright or Cypress skills
  • Performance testing → use dedicated performance tools
  • API contract testing → use OpenAPI/Pact patterns
推荐使用本技能的场景:
  • 为工具函数和实用程序编写单元测试
  • 使用React Testing Library创建组件测试
  • 为API调用、数据库或外部服务设置模拟
  • 集成测试模式
  • 了解测试覆盖率与CI配置
请勿使用本技能的场景:
  • Jest专属测试模式 → 两者类似,但请查看Jest文档了解差异
  • 端到端测试 → 使用Playwright或Cypress相关技能
  • 性能测试 → 使用专用性能测试工具
  • API契约测试 → 使用OpenAPI/Pact模式

Test Infrastructure

测试基础设施

Configuration:
vitest.config.ts
  • Environment: jsdom
  • Setup file:
    src/test/setup.ts
  • Coverage: v8 provider
Commands:
bash
npm test              # Watch mode
npm run test:run      # Single run
npm run test:coverage # With coverage
配置:
vitest.config.ts
  • 环境:jsdom
  • 初始化文件:
    src/test/setup.ts
  • 测试覆盖率:v8 provider
命令:
bash
npm test              # 监听模式
npm run test:run      # 单次运行
npm run test:coverage # 生成测试覆盖率报告

File Organization

文件组织结构

src/
├── app/api/__tests__/        # API route tests
├── components/__tests__/     # Component tests
├── lib/__tests__/            # Library/utility tests
└── lib/{feature}/__tests__/  # Feature-specific tests
Name tests as
{name}.test.ts
or
{name}.test.tsx
.
src/
├── app/api/__tests__/        # API路由测试
├── components/__tests__/     # 组件测试
├── lib/__tests__/            # 工具库/工具函数测试
└── lib/{feature}/__tests__/  # 特性专属测试
测试文件命名格式为
{name}.test.ts
{name}.test.tsx

Core Testing Patterns

核心测试模式

1. API Route Tests

1. API路由测试

typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GET, POST } from '../route';
import { NextRequest } from 'next/server';

// Mock dependencies
vi.mock('@/lib/auth', () => ({
  getSession: vi.fn(),
}));

vi.mock('@/db', () => ({
  db: {
    select: vi.fn().mockReturnThis(),
    from: vi.fn().mockReturnThis(),
    where: vi.fn().mockResolvedValue([]),
  },
}));

describe('GET /api/feature', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('returns 401 when not authenticated', async () => {
    vi.mocked(getSession).mockResolvedValue(null);

    const request = new NextRequest('http://localhost/api/feature');
    const response = await GET(request);

    expect(response.status).toBe(401);
  });

  it('returns data when authenticated', async () => {
    vi.mocked(getSession).mockResolvedValue({ userId: 'user-123' });
    vi.mocked(db.select).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockResolvedValue([{ id: '1', name: 'Test' }]),
      }),
    });

    const request = new NextRequest('http://localhost/api/feature');
    const response = await GET(request);
    const data = await response.json();

    expect(response.status).toBe(200);
    expect(data).toHaveLength(1);
  });
});
typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GET, POST } from '../route';
import { NextRequest } from 'next/server';

// Mock dependencies
vi.mock('@/lib/auth', () => ({
  getSession: vi.fn(),
}));

vi.mock('@/db', () => ({
  db: {
    select: vi.fn().mockReturnThis(),
    from: vi.fn().mockReturnThis(),
    where: vi.fn().mockResolvedValue([]),
  },
}));

describe('GET /api/feature', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('returns 401 when not authenticated', async () => {
    vi.mocked(getSession).mockResolvedValue(null);

    const request = new NextRequest('http://localhost/api/feature');
    const response = await GET(request);

    expect(response.status).toBe(401);
  });

  it('returns data when authenticated', async () => {
    vi.mocked(getSession).mockResolvedValue({ userId: 'user-123' });
    vi.mocked(db.select).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockResolvedValue([{ id: '1', name: 'Test' }]),
      }),
    });

    const request = new NextRequest('http://localhost/api/feature');
    const response = await GET(request);
    const data = await response.json();

    expect(response.status).toBe(200);
    expect(data).toHaveLength(1);
  });
});

2. Component Tests

2. 组件测试

typescript
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FeatureComponent } from '../FeatureComponent';

// Mock hooks
vi.mock('@/hooks/useAuth', () => ({
  useAuth: vi.fn().mockReturnValue({
    user: { id: 'user-123', name: 'Test User' },
    isLoading: false,
  }),
}));

describe('FeatureComponent', () => {
  it('renders loading state', () => {
    vi.mocked(useAuth).mockReturnValueOnce({
      user: null,
      isLoading: true,
    });

    render(<FeatureComponent />);
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
  });

  it('handles user interaction', async () => {
    const user = userEvent.setup();
    const onSubmit = vi.fn();

    render(<FeatureComponent onSubmit={onSubmit} />);

    await user.type(screen.getByRole('textbox'), 'Test input');
    await user.click(screen.getByRole('button', { name: /submit/i }));

    expect(onSubmit).toHaveBeenCalledWith('Test input');
  });

  it('displays error state', async () => {
    vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));

    render(<FeatureComponent />);

    await waitFor(() => {
      expect(screen.getByRole('alert')).toHaveTextContent(/error/i);
    });
  });
});
typescript
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FeatureComponent } from '../FeatureComponent';

// Mock hooks
vi.mock('@/hooks/useAuth', () => ({
  useAuth: vi.fn().mockReturnValue({
    user: { id: 'user-123', name: 'Test User' },
    isLoading: false,
  }),
}));

describe('FeatureComponent', () => {
  it('renders loading state', () => {
    vi.mocked(useAuth).mockReturnValueOnce({
      user: null,
      isLoading: true,
    });

    render(<FeatureComponent />);
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
  });

  it('handles user interaction', async () => {
    const user = userEvent.setup();
    const onSubmit = vi.fn();

    render(<FeatureComponent onSubmit={onSubmit} />);

    await user.type(screen.getByRole('textbox'), 'Test input');
    await user.click(screen.getByRole('button', { name: /submit/i }));

    expect(onSubmit).toHaveBeenCalledWith('Test input');
  });

  it('displays error state', async () => {
    vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));

    render(<FeatureComponent />);

    await waitFor(() => {
      expect(screen.getByRole('alert')).toHaveTextContent(/error/i);
    });
  });
});

3. Library/Utility Tests

3. 工具库/工具函数测试

typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { processData, formatDate } from '../utils';

describe('processData', () => {
  it('transforms input correctly', () => {
    const input = { raw: 'data' };
    const result = processData(input);

    expect(result).toEqual({
      processed: true,
      data: 'DATA',
    });
  });

  it('throws on invalid input', () => {
    expect(() => processData(null)).toThrow('Invalid input');
  });
});

describe('formatDate', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2025-01-15T10:00:00Z'));
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('formats relative dates', () => {
    const yesterday = new Date('2025-01-14T10:00:00Z');
    expect(formatDate(yesterday)).toBe('yesterday');
  });
});
typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { processData, formatDate } from '../utils';

describe('processData', () => {
  it('transforms input correctly', () => {
    const input = { raw: 'data' };
    const result = processData(input);

    expect(result).toEqual({
      processed: true,
      data: 'DATA',
    });
  });

  it('throws on invalid input', () => {
    expect(() => processData(null)).toThrow('Invalid input');
  });
});

describe('formatDate', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2025-01-15T10:00:00Z'));
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('formats relative dates', () => {
    const yesterday = new Date('2025-01-14T10:00:00Z');
    expect(formatDate(yesterday)).toBe('yesterday');
  });
});

Mocking Patterns

模拟模式

Module Mocking

模块模拟

typescript
// Mock entire module
vi.mock('@/lib/auth', () => ({
  getSession: vi.fn(),
  requireAuth: vi.fn(),
}));

// Mock with partial implementation
vi.mock('date-fns', async () => {
  const actual = await vi.importActual('date-fns');
  return {
    ...actual,
    format: vi.fn(() => '2025-01-15'),
  };
});

// Mock default export (like Anthropic SDK)
vi.mock('@anthropic-ai/sdk', () => ({
  default: class MockAnthropic {
    messages = {
      create: vi.fn().mockResolvedValue({
        content: [{ type: 'text', text: 'Mock response' }],
        usage: { input_tokens: 10, output_tokens: 20 },
      }),
    };
  },
}));
typescript
// Mock entire module
vi.mock('@/lib/auth', () => ({
  getSession: vi.fn(),
  requireAuth: vi.fn(),
}));

// Mock with partial implementation
vi.mock('date-fns', async () => {
  const actual = await vi.importActual('date-fns');
  return {
    ...actual,
    format: vi.fn(() => '2025-01-15'),
  };
});

// Mock default export (like Anthropic SDK)
vi.mock('@anthropic-ai/sdk', () => ({
  default: class MockAnthropic {
    messages = {
      create: vi.fn().mockResolvedValue({
        content: [{ type: 'text', text: 'Mock response' }],
        usage: { input_tokens: 10, output_tokens: 20 },
      }),
    };
  },
}));

Function Mocking

函数模拟

typescript
// Create mock function
const mockFn = vi.fn();

// Set return values
mockFn.mockReturnValue('sync value');
mockFn.mockResolvedValue('async value');
mockFn.mockRejectedValue(new Error('Failed'));

// One-time behavior
mockFn.mockReturnValueOnce('first call only');

// Custom implementation
mockFn.mockImplementation((arg) => arg.toUpperCase());

// Verify calls
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('expected', 'args');
typescript
// Create mock function
const mockFn = vi.fn();

// Set return values
mockFn.mockReturnValue('sync value');
mockFn.mockResolvedValue('async value');
mockFn.mockRejectedValue(new Error('Failed'));

// One-time behavior
mockFn.mockReturnValueOnce('first call only');

// Custom implementation
mockFn.mockImplementation((arg) => arg.toUpperCase());

// Verify calls
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('expected', 'args');

Chained Mock Pattern (Drizzle ORM)

链式模拟模式(Drizzle ORM)

typescript
vi.mock('@/db', () => ({
  db: {
    select: vi.fn().mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          orderBy: vi.fn().mockReturnValue({
            limit: vi.fn().mockResolvedValue([{ id: '1' }]),
          }),
        }),
      }),
    }),
    insert: vi.fn().mockReturnValue({
      values: vi.fn().mockReturnValue({
        returning: vi.fn().mockResolvedValue([{ id: 'new-1' }]),
      }),
    }),
  },
}));
typescript
vi.mock('@/db', () => ({
  db: {
    select: vi.fn().mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          orderBy: vi.fn().mockReturnValue({
            limit: vi.fn().mockResolvedValue([{ id: '1' }]),
          }),
        }),
      }),
    }),
    insert: vi.fn().mockReturnValue({
      values: vi.fn().mockReturnValue({
        returning: vi.fn().mockResolvedValue([{ id: 'new-1' }]),
      }),
    }),
  },
}));

Timer Mocking

定时器模拟

typescript
describe('debounced function', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('debounces calls', async () => {
    const callback = vi.fn();
    const debounced = debounce(callback, 300);

    debounced();
    debounced();
    debounced();

    expect(callback).not.toHaveBeenCalled();

    vi.advanceTimersByTime(300);

    expect(callback).toHaveBeenCalledTimes(1);
  });
});
typescript
describe('debounced function', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('debounces calls', async () => {
    const callback = vi.fn();
    const debounced = debounce(callback, 300);

    debounced();
    debounced();
    debounced();

    expect(callback).not.toHaveBeenCalled();

    vi.advanceTimersByTime(300);

    expect(callback).toHaveBeenCalledTimes(1);
  });
});

Query Priorities

查询优先级

Use queries in this order (most to least preferred):
  1. getByRole - Accessible queries (buttons, links, headings)
  2. getByLabelText - Form fields with labels
  3. getByPlaceholderText - Inputs with placeholders
  4. getByText - Non-interactive elements
  5. getByTestId - Last resort (data-testid)
typescript
// Preferred
screen.getByRole('button', { name: /submit/i });
screen.getByRole('heading', { level: 1 });
screen.getByLabelText(/email/i);

// Avoid unless necessary
screen.getByTestId('submit-button');
请按照以下优先级使用查询方法(从最推荐到最不推荐):
  1. getByRole - 可访问性查询(按钮、链接、标题等)
  2. getByLabelText - 带标签的表单字段
  3. getByPlaceholderText - 带占位符的输入框
  4. getByText - 非交互元素
  5. getByTestId - 最后选择(使用data-testid)
typescript
// Preferred
screen.getByRole('button', { name: /submit/i });
screen.getByRole('heading', { level: 1 });
screen.getByLabelText(/email/i);

// Avoid unless necessary
screen.getByTestId('submit-button');

Async Patterns

异步测试模式

typescript
// Wait for element to appear
await waitFor(() => {
  expect(screen.getByText('Loaded')).toBeInTheDocument();
});

// Find (built-in waitFor)
const element = await screen.findByText('Loaded');

// Wait for element to disappear
await waitFor(() => {
  expect(screen.queryByText('Loading')).not.toBeInTheDocument();
});
typescript
// Wait for element to appear
await waitFor(() => {
  expect(screen.getByText('Loaded')).toBeInTheDocument();
});

// Find (built-in waitFor)
const element = await screen.findByText('Loaded');

// Wait for element to disappear
await waitFor(() => {
  expect(screen.queryByText('Loading')).not.toBeInTheDocument();
});

Test Cleanup

测试清理

typescript
import { cleanup } from '@testing-library/react';

afterEach(() => {
  cleanup();            // React cleanup (automatic with setup.ts)
  vi.clearAllMocks();   // Reset mock call counts
  vi.resetAllMocks();   // Reset mocks to initial state
  vi.restoreAllMocks(); // Restore original implementations
});
typescript
import { cleanup } from '@testing-library/react';

afterEach(() => {
  cleanup();            // React组件清理(在setup.ts中自动执行)
  vi.clearAllMocks();   // 重置模拟函数的调用次数
  vi.resetAllMocks();   // 将模拟函数重置为初始状态
  vi.restoreAllMocks(); // 恢复原始实现
});

Accessibility Testing

可访问性测试

typescript
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

it('has no accessibility violations', async () => {
  const { container } = render(<Component />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
typescript
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

it('has no accessibility violations', async () => {
  const { container } = render(<Component />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Common Matchers

常用断言匹配器

typescript
// jest-dom matchers (from setup.ts)
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeDisabled();
expect(element).toHaveTextContent('text');
expect(element).toHaveAttribute('href', '/path');
expect(element).toHaveClass('active');
expect(input).toHaveValue('input value');
typescript
// jest-dom matchers (from setup.ts)
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeDisabled();
expect(element).toHaveTextContent('text');
expect(element).toHaveAttribute('href', '/path');
expect(element).toHaveClass('active');
expect(input).toHaveValue('input value');

References

参考资料