← Back to list

component-testing
by IvanTorresEdge
⭐ 0🍴 1📅 Jan 13, 2026
SKILL.md
name: component-testing description: React component testing with Vitest and Testing Library. Use when writing unit tests for components.
Component Testing Skill
This skill covers React component testing patterns with Vitest and Testing Library.
When to Use
Use this skill when:
- Writing unit tests for React components
- Testing user interactions
- Verifying component behavior
- Testing hooks and context
Core Principle
TEST USER BEHAVIOR - Test what users see and do, not implementation details. Your tests should not break when you refactor.
Setup
Installation
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
Configuration
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/__tests__/**/*.test.{ts,tsx}'],
coverage: {
provider: 'v8',
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
// src/test/setup.ts
import '@testing-library/jest-dom/vitest';
Basic Component Test
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Button } from '../Button';
describe('Button', () => {
it('renders children correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('applies variant class', () => {
render(<Button variant="primary">Primary</Button>);
expect(screen.getByRole('button')).toHaveClass('bg-primary');
});
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Disabled</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});
User Interaction Tests
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from '../Counter';
describe('Counter', () => {
it('increments count when button clicked', async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
await user.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('calls onChange with new count', async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<Counter onChange={handleChange} />);
await user.click(screen.getByRole('button', { name: /increment/i }));
expect(handleChange).toHaveBeenCalledWith(1);
});
});
Form Testing
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from '../LoginForm';
describe('LoginForm', () => {
it('submits form with email and password', async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
});
it('shows validation error for invalid email', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
await user.type(screen.getByLabelText(/email/i), 'invalid-email');
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(await screen.findByText(/invalid email/i)).toBeInTheDocument();
});
it('disables submit button while loading', async () => {
render(<LoginForm onSubmit={vi.fn()} isLoading />);
expect(screen.getByRole('button', { name: /sign in/i })).toBeDisabled();
});
});
Query Priority
Use queries in this order of preference:
// 1. Accessible queries (best)
screen.getByRole('button', { name: /submit/i });
screen.getByRole('textbox', { name: /email/i });
screen.getByRole('heading', { level: 1 });
// 2. Semantic queries
screen.getByLabelText(/email/i);
screen.getByPlaceholderText(/enter email/i);
screen.getByText(/welcome/i);
screen.getByAltText(/profile picture/i);
// 3. Test ID (last resort)
screen.getByTestId('custom-element');
Async Testing
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from '../UserProfile';
// Mock fetch
vi.mock('../api', () => ({
fetchUser: vi.fn(() =>
Promise.resolve({ id: '1', name: 'John Doe' })
),
}));
describe('UserProfile', () => {
it('displays loading state initially', () => {
render(<UserProfile userId="1" />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
it('displays user data after loading', async () => {
render(<UserProfile userId="1" />);
expect(await screen.findByText('John Doe')).toBeInTheDocument();
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
it('displays error when fetch fails', async () => {
const { fetchUser } = await import('../api');
vi.mocked(fetchUser).mockRejectedValueOnce(new Error('Failed'));
render(<UserProfile userId="1" />);
expect(await screen.findByText(/error/i)).toBeInTheDocument();
});
});
Testing with Context
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from '../ThemeContext';
import { ThemedButton } from '../ThemedButton';
function renderWithTheme(ui: React.ReactElement, theme = 'light') {
return render(
<ThemeProvider defaultTheme={theme}>
{ui}
</ThemeProvider>
);
}
describe('ThemedButton', () => {
it('renders with light theme styles', () => {
renderWithTheme(<ThemedButton>Click</ThemedButton>, 'light');
expect(screen.getByRole('button')).toHaveClass('bg-white');
});
it('renders with dark theme styles', () => {
renderWithTheme(<ThemedButton>Click</ThemedButton>, 'dark');
expect(screen.getByRole('button')).toHaveClass('bg-gray-900');
});
});
Testing Custom Hooks
import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useCounter } from '../useCounter';
describe('useCounter', () => {
it('initializes with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('initializes with provided value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('increments count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('resets to initial value', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
result.current.increment();
result.current.reset();
});
expect(result.current.count).toBe(5);
});
});
Mocking
Mocking Modules
import { vi } from 'vitest';
// Mock entire module
vi.mock('../api');
// Mock with implementation
vi.mock('../utils', () => ({
formatDate: vi.fn((date) => '2024-01-01'),
}));
Mocking Functions
const mockFn = vi.fn();
mockFn.mockReturnValue('default');
mockFn.mockReturnValueOnce('first call');
mockFn.mockResolvedValue({ data: 'async result' });
mockFn.mockRejectedValue(new Error('Failed'));
Test Structure
File Organization
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── index.ts
│ │ └── __tests__/
│ │ └── Button.test.tsx
Test Naming
// Use verb-first naming
it('renders children correctly', ...);
it('calls onClick when clicked', ...);
it('throws error for invalid input', ...);
it('displays loading state initially', ...);
Best Practices
- Test behavior, not implementation - Focus on what users experience
- Use accessible queries - getByRole, getByLabelText first
- Avoid testing styles - Test classes/attributes, not computed styles
- Keep tests focused - One behavior per test
- Use userEvent over fireEvent - More realistic interactions
- Wait for async operations - Use findBy, waitFor
Notes
- Testing Library encourages accessible components
- Avoid snapshot tests for components (too brittle)
- Mock at the boundary (API, not internal functions)
- Tests should give confidence to refactor
Score
Total Score
60/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
✓LICENSE
ライセンスが設定されている
+10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon