如何在 Astro 项目中进行测试?如何使用 Vitest、Playwright 等测试框架?
Astro 的测试策略对于确保代码质量和应用稳定性至关重要。了解如何在 Astro 项目中进行单元测试、集成测试和端到端测试是开发者必备的技能。测试框架选择:Vitest(推荐):与 Vite 深度集成快速的测试执行支持 TypeScriptJest:成熟的测试框架丰富的生态系统广泛使用Playwright:端到端测试跨浏览器支持现代化的 API安装测试依赖:# 安装 Vitestnpm install -D vitest @vitest/ui# 安装测试工具npm install -D @testing-library/react @testing-library/vue @testing-library/svelte# 安装 Playwrightnpm install -D @playwright/test配置测试环境:// vitest.config.tsimport { defineConfig } from 'vitest/config';import astro from 'astro/vitest';export default defineConfig({ plugins: [astro()], test: { environment: 'jsdom', globals: true, setupFiles: ['./src/test/setup.ts'], },});// src/test/setup.tsimport { expect, afterEach } from 'vitest';import { cleanup } from '@testing-library/react';// 清理测试环境afterEach(() => { cleanup();});// 扩展 expectexpect.extend({});单元测试 Astro 组件:// src/components/__tests__/Button.astro.test.tsimport { describe, it, expect } from 'vitest';import { render } from '@testing-library/react';import Button from '../Button.astro';describe('Button Component', () => { it('renders button with correct text', () => { const { getByText } = render(Button, { props: { text: 'Click me' }, }); expect(getByText('Click me')).toBeInTheDocument(); }); it('applies correct variant class', () => { const { container } = render(Button, { props: { variant: 'primary' }, }); const button = container.querySelector('button'); expect(button).toHaveClass('btn-primary'); });});测试 React 组件:// src/components/__tests__/Counter.test.tsximport { describe, it, expect, vi } from 'vitest';import { render, screen, fireEvent } from '@testing-library/react';import userEvent from '@testing-library/user-event';import Counter from '../Counter';describe('Counter Component', () => { it('renders initial count', () => { render(<Counter initialCount={0} />); expect(screen.getByText('Count: 0')).toBeInTheDocument(); }); it('increments count when button is clicked', async () => { const user = userEvent.setup(); render(<Counter initialCount={0} />); const button = screen.getByRole('button', { name: 'Increment' }); await user.click(button); expect(screen.getByText('Count: 1')).toBeInTheDocument(); });});测试 Vue 组件:// src/components/__tests__/TodoList.test.tsimport { describe, it, expect } from 'vitest';import { mount } from '@vue/test-utils';import TodoList from '../TodoList.vue';describe('TodoList Component', () => { it('renders todo items', () => { const todos = [ { id: 1, text: 'Learn Astro', completed: false }, { id: 2, text: 'Build app', completed: true }, ]; const wrapper = mount(TodoList, { props: { todos }, }); expect(wrapper.findAll('.todo-item')).toHaveLength(2); expect(wrapper.text()).toContain('Learn Astro'); }); it('emits complete event when checkbox is clicked', async () => { const wrapper = mount(TodoList, { props: { todos: [{ id: 1, text: 'Task', completed: false }] }, }); await wrapper.find('input[type="checkbox"]').setValue(true); expect(wrapper.emitted('complete')).toBeTruthy(); expect(wrapper.emitted('complete')[0]).toEqual([1]); });});测试 API 路由:// src/pages/api/__tests__/users.test.tsimport { describe, it, expect, beforeEach, vi } from 'vitest';import { GET, POST } from '../users';describe('Users API', () => { beforeEach(() => { vi.clearAllMocks(); }); it('GET returns list of users', async () => { const request = new Request('http://localhost/api/users'); const response = await GET({ request } as any); const data = await response.json(); expect(response.status).toBe(200); expect(data).toHaveProperty('users'); expect(Array.isArray(data.users)).toBe(true); }); it('POST creates new user', async () => { const userData = { name: 'John Doe', email: 'john@example.com' }; const request = new Request('http://localhost/api/users', { method: 'POST', body: JSON.stringify(userData), }); const response = await POST({ request } as any); const data = await response.json(); expect(response.status).toBe(201); expect(data).toHaveProperty('id'); expect(data.name).toBe(userData.name); });});测试内容集合:// src/content/__tests__/blog.test.tsimport { describe, it, expect } from 'vitest';import { getCollection } from 'astro:content';describe('Blog Content Collection', () => { it('has required frontmatter fields', async () => { const posts = await getCollection('blog'); posts.forEach(post => { expect(post.data).toHaveProperty('title'); expect(post.data).toHaveProperty('publishDate'); expect(post.data).toHaveProperty('description'); }); }); it('has valid publish dates', async () => { const posts = await getCollection('blog'); posts.forEach(post => { expect(post.data.publishDate).toBeInstanceOf(Date); expect(post.data.publishDate.getTime()).not.toBeNaN(); }); });});端到端测试(Playwright):// e2e/home.spec.tsimport { test, expect } from '@playwright/test';test.describe('Home Page', () => { test('loads successfully', async ({ page }) => { await page.goto('/'); await expect(page).toHaveTitle(/My Astro App/); await expect(page.locator('h1')).toContainText('Welcome'); }); test('navigation works', async ({ page }) => { await page.goto('/'); await page.click('text=About'); await expect(page).toHaveURL(/\/about/); await expect(page.locator('h1')).toContainText('About Us'); }); test('form submission', async ({ page }) => { await page.goto('/contact'); await page.fill('input[name="name"]', 'John Doe'); await page.fill('input[name="email"]', 'john@example.com'); await page.fill('textarea[name="message"]', 'Hello!'); await page.click('button[type="submit"]'); await expect(page.locator('.success-message')).toBeVisible(); });});测试中间件:// src/middleware/__tests__/auth.test.tsimport { describe, it, expect, vi } from 'vitest';import { onRequest } from '../middleware';describe('Auth Middleware', () => { it('redirects to login without token', async () => { const request = new Request('http://localhost/dashboard'); const redirectSpy = vi.fn(); await onRequest({ request, redirect: redirectSpy } as any); expect(redirectSpy).toHaveBeenCalledWith('/login'); }); it('allows access with valid token', async () => { const request = new Request('http://localhost/dashboard', { headers: { 'Authorization': 'Bearer valid-token' }, }); const nextSpy = vi.fn().mockResolvedValue(new Response()); await onRequest({ request, next: nextSpy } as any); expect(nextSpy).toHaveBeenCalled(); });});测试配置脚本:// package.json{ "scripts": { "test": "vitest", "test:ui": "vitest --ui", "test:run": "vitest run", "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:headed": "playwright test --headed" }}测试覆盖率:// vitest.config.tsimport { defineConfig } from 'vitest/config';import astro from 'astro/vitest';export default defineConfig({ plugins: [astro()], test: { coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/', 'src/test/', '**/*.d.ts', '**/*.config.*', '**/mockData', ], }, },});最佳实践:测试金字塔:大量单元测试适量集成测试少量端到端测试测试组织:按功能组织测试使用清晰的测试名称保持测试独立Mock 和 Stub:隔离外部依赖使用 vi.mock() 模拟模块提供一致的测试数据持续集成:在 CI 中运行测试设置测试覆盖率阈值自动化测试报告测试性能:使用测试缓存并行运行测试优化测试执行时间Astro 的测试生态系统提供了全面的测试支持,帮助开发者构建可靠的应用。