# 02-测试技术 ## 📋 学习目标 - 掌握单元测试编写 - 学习组件测试技巧 - 理解E2E测试流程 - 掌握测试覆盖率分析 ## 🧪 测试框架 ### Vitest(推荐) ```bash # 安装 pnpm add -D vitest @vitest/ui # package.json { "scripts": { "test": "vitest", "test:ui": "vitest --ui", "test:coverage": "vitest --coverage" } } ``` ### 基本配置 ```javascript // vitest.config.ts import {defineConfig} from 'vitest/config'; export default defineConfig({ test: { environment: 'jsdom', // 或'node', 'happy-dom' globals: true, setupFiles: './tests/setup.ts', coverage: { provider: 'v8', reporter: ['text', 'json', 'html'] } } }); ``` ## 📝 单元测试 ### 基本断言 ```javascript import {describe, it, expect} from 'vitest'; describe('Math utils', () => { it('should add two numbers', () => { expect(1 + 1).toBe(2); }); it('should multiply numbers', () => { const result = multiply(3, 4); expect(result).toBe(12); expect(result).toBeGreaterThan(10); expect(result).toBeLessThan(20); }); it('should handle objects', () => { const user = {name: 'John', age: 30}; expect(user).toEqual({name: 'John', age: 30}); expect(user).toHaveProperty('name'); expect(user).toMatchObject({name: 'John'}); }); it('should handle arrays', () => { const arr = [1, 2, 3]; expect(arr).toHaveLength(3); expect(arr).toContain(2); expect(arr).toEqual([1, 2, 3]); }); }); ``` ### Mock函数 ```javascript import {vi} from 'vitest'; describe('Mock functions', () => { it('should track calls', () => { const mockFn = vi.fn(); mockFn('hello'); mockFn('world'); expect(mockFn).toHaveBeenCalledTimes(2); expect(mockFn).toHaveBeenCalledWith('hello'); expect(mockFn).toHaveBeenLastCalledWith('world'); }); it('should return values', () => { const mockFn = vi.fn() .mockReturnValueOnce('first') .mockReturnValueOnce('second') .mockReturnValue('default'); expect(mockFn()).toBe('first'); expect(mockFn()).toBe('second'); expect(mockFn()).toBe('default'); }); it('should mock implementation', () => { const mockFn = vi.fn((a, b) => a + b); expect(mockFn(1, 2)).toBe(3); }); }); ``` ### 异步测试 ```javascript describe('Async tests', () => { it('should handle promises', async () => { const data = await fetchData(); expect(data).toEqual({id: 1}); }); it('should handle async/await', async () => { const result = await asyncFunction(); expect(result).toBe('success'); }); it('should handle errors', async () => { await expect(failingAsync()).rejects.toThrow('Error'); }); it('should timeout', async () => { await new Promise(resolve => setTimeout(resolve, 100)); }, {timeout: 1000}); }); ``` ## ⚛️ React组件测试 ### React Testing Library ```bash # 安装 pnpm add -D @testing-library/react @testing-library/jest-dom ``` ### 基本测试 ```jsx import {render, screen, fireEvent} from '@testing-library/react'; import {describe, it, expect} from 'vitest'; import Counter from './Counter'; describe('Counter', () => { it('should render initial count', () => { render(); expect(screen.getByText('Count: 0')).toBeInTheDocument(); }); it('should increment count', () => { render(); const button = screen.getByRole('button', {name: /increment/i}); fireEvent.click(button); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); it('should decrement count', async () => { render(); const button = screen.getByRole('button', {name: /decrement/i}); await userEvent.click(button); expect(screen.getByText('Count: -1')).toBeInTheDocument(); }); }); ``` ### 测试Hooks ```javascript import {renderHook, act} from '@testing-library/react'; import {useCounter} from './useCounter'; describe('useCounter', () => { it('should initialize with 0', () => { const {result} = renderHook(() => useCounter()); expect(result.current.count).toBe(0); }); it('should increment', () => { const {result} = renderHook(() => useCounter()); act(() => { result.current.increment(); }); expect(result.current.count).toBe(1); }); }); ``` ### 测试异步组件 ```jsx import {render, screen, waitFor} from '@testing-library/react'; describe('UserList', () => { it('should load and display users', async () => { render(); // 等待加载完成 await waitFor(() => { expect(screen.getByText('John')).toBeInTheDocument(); }); }); it('should handle errors', async () => { // Mock API错误 vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('API Error')); render(); await waitFor(() => { expect(screen.getByText(/error/i)).toBeInTheDocument(); }); }); }); ``` ## 💚 Vue组件测试 ### Vue Test Utils ```bash # 安装 pnpm add -D @vue/test-utils ``` ### 基本测试 ```javascript import {mount} from '@vue/test-utils'; import {describe, it, expect} from 'vitest'; import Counter from './Counter.vue'; describe('Counter.vue', () => { it('should render count', () => { const wrapper = mount(Counter); expect(wrapper.text()).toContain('Count: 0'); }); it('should increment', async () => { const wrapper = mount(Counter); await wrapper.find('button').trigger('click'); expect(wrapper.text()).toContain('Count: 1'); }); it('should accept props', () => { const wrapper = mount(Counter, { props: { initialCount: 10 } }); expect(wrapper.text()).toContain('Count: 10'); }); }); ``` ### 测试组合式函数 ```javascript import {useCounter} from './useCounter'; describe('useCounter', () => { it('should increment', () => { const {count, increment} = useCounter(); expect(count.value).toBe(0); increment(); expect(count.value).toBe(1); }); }); ``` ## 🌐 E2E测试 ### Playwright(推荐) ```bash # 安装 pnpm create playwright # 运行测试 pnpm playwright test pnpm playwright test --ui pnpm playwright show-report ``` ### 基本E2E测试 ```javascript import {test, expect} from '@playwright/test'; test.describe('Login flow', () => { test('should login successfully', async ({page}) => { // 访问页面 await page.goto('http://localhost:3000/login'); // 填写表单 await page.fill('input[name="email"]', 'user@example.com'); await page.fill('input[name="password"]', 'password123'); // 点击按钮 await page.click('button[type="submit"]'); // 验证跳转 await expect(page).toHaveURL('http://localhost:3000/dashboard'); // 验证元素 await expect(page.locator('h1')).toContainText('Dashboard'); }); test('should show error for invalid credentials', async ({page}) => { await page.goto('http://localhost:3000/login'); await page.fill('input[name="email"]', 'wrong@example.com'); await page.fill('input[name="password"]', 'wrong'); await page.click('button[type="submit"]'); await expect(page.locator('.error')).toBeVisible(); }); }); ``` ### 高级E2E测试 ```javascript test.describe('Shopping cart', () => { test.beforeEach(async ({page}) => { // 登录 await page.goto('/login'); await page.fill('[name="email"]', 'user@example.com'); await page.fill('[name="password"]', 'password'); await page.click('button[type="submit"]'); await page.waitForURL('/dashboard'); }); test('should add item to cart', async ({page}) => { await page.goto('/products'); // 点击第一个产品 await page.click('.product-card:first-child button'); // 验证购物车数量 const badge = page.locator('.cart-badge'); await expect(badge).toHaveText('1'); // 截图 await page.screenshot({path: 'cart.png'}); }); }); ``` ## 📊 测试覆盖率 ### 配置覆盖率 ```javascript // vitest.config.ts export default defineConfig({ test: { coverage: { provider: 'v8', reporter: ['text', 'json', 'html', 'lcov'], exclude: [ 'node_modules/', 'tests/', '**/*.test.{js,ts,jsx,tsx}', '**/*.config.{js,ts}', '**/types.ts' ], thresholds: { lines: 80, functions: 80, branches: 80, statements: 80 } } } }); ``` ### 查看覆盖率 ```bash # 生成覆盖率报告 pnpm test:coverage # 打开HTML报告 open coverage/index.html ``` ## 🎯 测试最佳实践 ### 1. AAA模式 ```javascript test('should add item to list', () => { // Arrange(准备) const list = new List(); const item = {id: 1, name: 'Item'}; // Act(执行) list.add(item); // Assert(断言) expect(list.items).toHaveLength(1); expect(list.items[0]).toEqual(item); }); ``` ### 2. 测试隔离 ```javascript describe('User service', () => { let userService; beforeEach(() => { // 每个测试前重置 userService = new UserService(); }); afterEach(() => { // 清理 userService.clear(); }); it('should create user', () => { const user = userService.create('John'); expect(user.name).toBe('John'); }); }); ``` ### 3. Mock外部依赖 ```javascript import {vi} from 'vitest'; import {fetchUser} from './api'; vi.mock('./api', () => ({ fetchUser: vi.fn() })); test('should fetch user', async () => { fetchUser.mockResolvedValue({id: 1, name: 'John'}); const user = await getUser(1); expect(fetchUser).toHaveBeenCalledWith(1); expect(user.name).toBe('John'); }); ``` ### 4. 快照测试 ```tsx import {render} from '@testing-library/react'; test('should match snapshot', () => { const {container} = render(); expect(container).toMatchSnapshot(); }); ``` ## 🔄 持续集成 ### GitHub Actions ```yaml # .github/workflows/test.yml name: Test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '20' - name: Install dependencies run: pnpm install - name: Run tests run: pnpm test:coverage - name: Upload coverage uses: codecov/codecov-action@v3 ``` ## 📚 测试工具推荐 ### 单元测试 - Vitest - Jest - Mocha + Chai ### 组件测试 - React Testing Library - Vue Test Utils - Enzyme(React旧项目) ### E2E测试 - Playwright - Cypress - Puppeteer ### 其他工具 - MSW:API Mock - Faker.js:测试数据生成 - Testing Playground:选择器辅助 ## 📚 参考资料 - [Vitest文档](https://vitest.dev/) - [React Testing Library](https://testing-library.com/react) - [Vue Test Utils](https://test-utils.vuejs.org/) - [Playwright文档](https://playwright.dev/)