测试策略 #
一、测试工具配置 #
1.1 安装测试依赖 #
bash
npm install --save-dev jest @testing-library/react @testing-library/jest-dom jest-styled-components
1.2 Jest 配置 #
javascript
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^styled-components$': 'styled-components',
},
transform: {
'^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],
},
};
1.3 Jest Setup 文件 #
javascript
import '@testing-library/jest-dom';
import 'jest-styled-components';
二、组件测试 #
2.1 基础渲染测试 #
tsx
import { render, screen } from '@testing-library/react';
import styled from 'styled-components';
import '@testing-library/jest-dom';
const Button = styled.button`
padding: 12px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
`;
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toBeInTheDocument();
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('handles click events', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
screen.getByRole('button').click();
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
2.2 Props 测试 #
tsx
import { render, screen } from '@testing-library/react';
import styled from 'styled-components';
interface ButtonProps {
$variant?: 'primary' | 'secondary';
disabled?: boolean;
}
const Button = styled.button<ButtonProps>`
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
${props => props.$variant === 'primary' && `
background: #667eea;
color: white;
`}
${props => props.$variant === 'secondary' && `
background: transparent;
color: #667eea;
border: 2px solid #667eea;
`}
${props => props.disabled && `
opacity: 0.6;
cursor: not-allowed;
`}
`;
describe('Button variants', () => {
it('renders primary variant', () => {
render(<Button $variant="primary">Primary</Button>);
const button = screen.getByRole('button');
expect(button).toHaveStyleRule('background', '#667eea');
expect(button).toHaveStyleRule('color', 'white');
});
it('renders secondary variant', () => {
render(<Button $variant="secondary">Secondary</Button>);
const button = screen.getByRole('button');
expect(button).toHaveStyleRule('background', 'transparent');
expect(button).toHaveStyleRule('color', '#667eea');
});
it('renders disabled state', () => {
render(<Button disabled>Disabled</Button>);
const button = screen.getByRole('button');
expect(button).toHaveStyleRule('opacity', '0.6');
expect(button).toBeDisabled();
});
});
2.3 主题测试 #
tsx
import { render, screen } from '@testing-library/react';
import styled, { ThemeProvider } from 'styled-components';
const theme = {
colors: {
primary: '#667eea',
secondary: '#764ba2',
},
};
const ThemedButton = styled.button`
background: ${props => props.theme.colors.primary};
color: white;
padding: 12px 24px;
`;
describe('ThemedButton', () => {
it('uses theme colors', () => {
render(
<ThemeProvider theme={theme}>
<ThemedButton>Themed</ThemedButton>
</ThemeProvider>
);
const button = screen.getByRole('button');
expect(button).toHaveStyleRule('background', '#667eea');
});
});
三、快照测试 #
3.1 基础快照 #
tsx
import { render } from '@testing-library/react';
import styled from 'styled-components';
const Card = styled.div`
padding: 24px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
`;
describe('Card', () => {
it('matches snapshot', () => {
const { container } = render(
<Card>
<h2>Title</h2>
<p>Content</p>
</Card>
);
expect(container).toMatchSnapshot();
});
});
3.2 变体快照 #
tsx
import { render } from '@testing-library/react';
import styled from 'styled-components';
const Button = styled.button<{ $variant?: string }>`
padding: 12px 24px;
border-radius: 8px;
${props => props.$variant === 'primary' && `
background: #667eea;
color: white;
`}
${props => props.$variant === 'secondary' && `
background: transparent;
color: #667eea;
`}
`;
describe('Button snapshots', () => {
it('matches primary snapshot', () => {
const { container } = render(<Button $variant="primary">Primary</Button>);
expect(container).toMatchSnapshot();
});
it('matches secondary snapshot', () => {
const { container } = render(<Button $variant="secondary">Secondary</Button>);
expect(container).toMatchSnapshot();
});
});
四、样式测试 #
4.1 使用 jest-styled-components #
tsx
import { render, screen } from '@testing-library/react';
import styled from 'styled-components';
import 'jest-styled-components';
const Button = styled.button`
padding: 12px 24px;
background: #667eea;
color: white;
border-radius: 8px;
&:hover {
background: #5a6fd6;
}
`;
describe('Button styles', () => {
it('has correct base styles', () => {
render(<Button>Click</Button>);
const button = screen.getByRole('button');
expect(button).toHaveStyleRule('padding', '12px 24px');
expect(button).toHaveStyleRule('background', '#667eea');
expect(button).toHaveStyleRule('color', 'white');
expect(button).toHaveStyleRule('border-radius', '8px');
});
it('has correct hover styles', () => {
render(<Button>Click</Button>);
const button = screen.getByRole('button');
expect(button).toHaveStyleRule('background', '#5a6fd6', {
modifier: ':hover',
});
});
});
4.2 媒体查询测试 #
tsx
import { render, screen } from '@testing-library/react';
import styled from 'styled-components';
import 'jest-styled-components';
const Container = styled.div`
padding: 16px;
@media (min-width: 768px) {
padding: 24px;
}
@media (min-width: 1024px) {
padding: 32px;
}
`;
describe('Container responsive styles', () => {
it('has correct media query styles', () => {
render(<Container>Content</Container>);
const container = screen.getByText('Content');
expect(container).toHaveStyleRule('padding', '24px', {
media: '(min-width: 768px)',
});
expect(container).toHaveStyleRule('padding', '32px', {
media: '(min-width: 1024px)',
});
});
});
4.3 嵌套选择器测试 #
tsx
import { render, screen } from '@testing-library/react';
import styled from 'styled-components';
import 'jest-styled-components';
const Card = styled.div`
padding: 24px;
h2 {
font-size: 24px;
color: #333;
}
p {
color: #666;
}
a {
color: #667eea;
&:hover {
text-decoration: underline;
}
}
`;
describe('Card nested styles', () => {
it('has correct nested selector styles', () => {
render(<Card>Content</Card>);
const card = screen.getByText('Content');
expect(card).toHaveStyleRule('font-size', '24px', {
modifier: 'h2',
});
expect(card).toHaveStyleRule('color', '#666', {
modifier: 'p',
});
expect(card).toHaveStyleRule('color', '#667eea', {
modifier: 'a',
});
expect(card).toHaveStyleRule('text-decoration', 'underline', {
modifier: 'a:hover',
});
});
});
五、交互测试 #
5.1 用户交互测试 #
tsx
import { render, screen, fireEvent } from '@testing-library/react';
import styled from 'styled-components';
import userEvent from '@testing-library/user-event';
const ToggleButton = styled.button<{ $active?: boolean }>`
padding: 12px 24px;
background: ${props => props.$active ? '#667eea' : '#e0e0e0'};
color: ${props => props.$active ? 'white' : '#333'};
border: none;
border-radius: 8px;
cursor: pointer;
`;
function Toggle() {
const [active, setActive] = useState(false);
return (
<ToggleButton
$active={active}
onClick={() => setActive(!active)}
>
{active ? 'Active' : 'Inactive'}
</ToggleButton>
);
}
describe('Toggle interaction', () => {
it('toggles active state on click', async () => {
const user = userEvent.setup();
render(<Toggle />);
const button = screen.getByRole('button');
expect(button).toHaveTextContent('Inactive');
expect(button).toHaveStyleRule('background', '#e0e0e0');
await user.click(button);
expect(button).toHaveTextContent('Active');
expect(button).toHaveStyleRule('background', '#667eea');
});
});
5.2 表单测试 #
tsx
import { render, screen, fireEvent } from '@testing-library/react';
import styled from 'styled-components';
import userEvent from '@testing-library/user-event';
const StyledInput = styled.input`
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
&:focus {
border-color: #667eea;
}
`;
function Form() {
const [value, setValue] = useState('');
return (
<form>
<StyledInput
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Enter text"
data-testid="input"
/>
<span data-testid="value">{value}</span>
</form>
);
}
describe('Form input', () => {
it('updates value on change', async () => {
const user = userEvent.setup();
render(<Form />);
const input = screen.getByTestId('input');
await user.type(input, 'Hello');
expect(screen.getByTestId('value')).toHaveTextContent('Hello');
});
});
六、主题测试 #
6.1 主题提供者测试 #
tsx
import { render, screen } from '@testing-library/react';
import styled, { ThemeProvider } from 'styled-components';
import 'jest-styled-components';
const lightTheme = {
colors: {
primary: '#667eea',
background: '#ffffff',
text: '#333333',
},
};
const darkTheme = {
colors: {
primary: '#818cf8',
background: '#111827',
text: '#f9fafb',
},
};
const ThemedCard = styled.div`
background: ${props => props.theme.colors.background};
color: ${props => props.theme.colors.text};
padding: 24px;
`;
describe('ThemedCard', () => {
it('renders with light theme', () => {
render(
<ThemeProvider theme={lightTheme}>
<ThemedCard>Content</ThemedCard>
</ThemeProvider>
);
const card = screen.getByText('Content');
expect(card).toHaveStyleRule('background', '#ffffff');
expect(card).toHaveStyleRule('color', '#333333');
});
it('renders with dark theme', () => {
render(
<ThemeProvider theme={darkTheme}>
<ThemedCard>Content</ThemedCard>
</ThemeProvider>
);
const card = screen.getByText('Content');
expect(card).toHaveStyleRule('background', '#111827');
expect(card).toHaveStyleRule('color', '#f9fafb');
});
});
6.2 主题切换测试 #
tsx
import { render, screen, fireEvent } from '@testing-library/react';
import styled, { ThemeProvider } from 'styled-components';
import userEvent from '@testing-library/user-event';
const themes = {
light: { background: '#ffffff', text: '#333333' },
dark: { background: '#111827', text: '#f9fafb' },
};
const Container = styled.div`
background: ${props => props.theme.background};
color: ${props => props.theme.text};
padding: 24px;
`;
function ThemedApp() {
const [isDark, setIsDark] = useState(false);
return (
<ThemeProvider theme={isDark ? themes.dark : themes.light}>
<Container>
<button onClick={() => setIsDark(!isDark)}>
Toggle Theme
</button>
</Container>
</ThemeProvider>
);
}
describe('Theme switching', () => {
it('switches between themes', async () => {
const user = userEvent.setup();
render(<ThemedApp />);
const container = screen.getByRole('button').parentElement;
const button = screen.getByText('Toggle Theme');
expect(container).toHaveStyleRule('background', '#ffffff');
await user.click(button);
expect(container).toHaveStyleRule('background', '#111827');
});
});
七、测试工具函数 #
7.1 自定义渲染函数 #
tsx
import { render, RenderOptions } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import React from 'react';
const defaultTheme = {
colors: {
primary: '#667eea',
background: '#ffffff',
text: '#333333',
},
};
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
theme?: typeof defaultTheme;
}
export function renderWithTheme(
ui: React.ReactElement,
options: CustomRenderOptions = {}
) {
const { theme = defaultTheme, ...renderOptions } = options;
return render(ui, {
wrapper: ({ children }) => (
<ThemeProvider theme={theme}>{children}</ThemeProvider>
),
...renderOptions,
});
}
7.2 使用自定义渲染 #
tsx
import { screen } from '@testing-library/react';
import { renderWithTheme } from './test-utils';
import styled from 'styled-components';
const Button = styled.button`
background: ${props => props.theme.colors.primary};
color: white;
padding: 12px 24px;
`;
describe('Button with custom render', () => {
it('renders with theme', () => {
renderWithTheme(<Button>Click</Button>);
const button = screen.getByRole('button');
expect(button).toHaveStyleRule('background', '#667eea');
});
});
八、总结 #
测试策略要点速查表:
| 测试类型 | 工具 | 用途 |
|---|---|---|
| 组件测试 | @testing-library/react | 渲染和交互测试 |
| 快照测试 | Jest | UI 回归测试 |
| 样式测试 | jest-styled-components | CSS 规则验证 |
| 主题测试 | ThemeProvider | 主题切换验证 |
下一步:学习 服务端渲染 掌握 SSR 中的样式处理。
最后更新:2026-03-28