乐闻世界logo
搜索文章和话题

如何在 Astro 项目中进行测试?如何使用 Vitest、Playwright 等测试框架?

2月21日 15:18

Astro 的测试策略对于确保代码质量和应用稳定性至关重要。了解如何在 Astro 项目中进行单元测试、集成测试和端到端测试是开发者必备的技能。

测试框架选择:

  1. Vitest(推荐):

    • 与 Vite 深度集成
    • 快速的测试执行
    • 支持 TypeScript
  2. Jest

    • 成熟的测试框架
    • 丰富的生态系统
    • 广泛使用
  3. Playwright

    • 端到端测试
    • 跨浏览器支持
    • 现代化的 API

安装测试依赖:

bash
# 安装 Vitest npm install -D vitest @vitest/ui # 安装测试工具 npm install -D @testing-library/react @testing-library/vue @testing-library/svelte # 安装 Playwright npm install -D @playwright/test

配置测试环境:

javascript
// vitest.config.ts import { defineConfig } from 'vitest/config'; import astro from 'astro/vitest'; export default defineConfig({ plugins: [astro()], test: { environment: 'jsdom', globals: true, setupFiles: ['./src/test/setup.ts'], }, });
typescript
// src/test/setup.ts import { expect, afterEach } from 'vitest'; import { cleanup } from '@testing-library/react'; // 清理测试环境 afterEach(() => { cleanup(); }); // 扩展 expect expect.extend({});

单元测试 Astro 组件:

typescript
// src/components/__tests__/Button.astro.test.ts import { 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 组件:

typescript
// src/components/__tests__/Counter.test.tsx import { 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 组件:

typescript
// src/components/__tests__/TodoList.test.ts import { 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 路由:

typescript
// src/pages/api/__tests__/users.test.ts import { 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); }); });

测试内容集合:

typescript
// src/content/__tests__/blog.test.ts import { 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):

typescript
// e2e/home.spec.ts import { 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(); }); });

测试中间件:

typescript
// src/middleware/__tests__/auth.test.ts import { 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(); }); });

测试配置脚本:

json
// 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" } }

测试覆盖率:

javascript
// vitest.config.ts import { 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', ], }, }, });

最佳实践:

  1. 测试金字塔

    • 大量单元测试
    • 适量集成测试
    • 少量端到端测试
  2. 测试组织

    • 按功能组织测试
    • 使用清晰的测试名称
    • 保持测试独立
  3. Mock 和 Stub

    • 隔离外部依赖
    • 使用 vi.mock() 模拟模块
    • 提供一致的测试数据
  4. 持续集成

    • 在 CI 中运行测试
    • 设置测试覆盖率阈值
    • 自动化测试报告
  5. 测试性能

    • 使用测试缓存
    • 并行运行测试
    • 优化测试执行时间

Astro 的测试生态系统提供了全面的测试支持,帮助开发者构建可靠的应用。

标签:Astro