测试策略 #

一、测试工具配置 #

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