5月27日 15:38

如何在 Astro 项目中使用 Vitest 和 Playwright 进行测试?

Astro 项目中的测试涉及单元测试、组件测试和端到端测试三个层次,不同层次的测试需要搭配不同的工具和策略。实际开发中,Vitest 负责 .ts 工具函数和框架组件的单元测试,Playwright 负责页面级别的端到端测试,而 .astro 组件的测试则需要借助 Container API 或 vitest-browser-astro 方案。

测试框架选型

Vitest 是 Astro 官方推荐的单元测试框架,它基于 Vite 构建,与 Astro 的构建体系天然兼容,配置简单且执行速度快。如果你的项目已经使用 Vite,Vitest 几乎可以零配置接入。

Playwright 是端到端测试的首选,支持 Chromium、Firefox 和 WebKit 三大浏览器引擎,能够在真实浏览器环境中模拟用户操作,验证页面导航、表单提交、交互逻辑等完整流程。

Jest 虽然生态成熟,但需要额外的 transform 配置才能处理 Astro 项目中的 TypeScript 和 JSX,配置成本较高,一般不推荐在 Astro 项目中使用。

安装测试依赖

bash
# 安装 Vitest npm install -D vitest # 安装 vitest-browser-astro(用于测试 .astro 组件) npm install -D vitest-browser-astro @vitest/browser-playwright playwright # 安装框架组件测试工具(按需选择) npm install -D @testing-library/react # React 组件 npm install -D @testing-library/vue # Vue 组件 npm install -D @vue/test-utils # Vue 组件(替代方案) # 安装 Playwright npm install -D @playwright/test npx playwright install

配置 Vitest

Astro 提供了 getViteConfig() 辅助函数,可以自动将项目的 Astro 配置应用到 Vitest,避免手动对齐 Vite 插件和路径别名。

typescript
// vitest.config.ts /// <reference types="vitest" /> import { getViteConfig } from 'astro/config'; export default getViteConfig({ test: { // 服务端代码测试用 node 环境 environment: 'node', globals: true, setupFiles: ['./src/test/setup.ts'], }, });

如果部分测试依赖 DOM 环境,可以在单个测试文件顶部通过注释指定:

typescript
// @vitest-environment jsdom import { describe, it, expect } from 'vitest';

getViteConfig() 还支持第二个参数,用于在测试中覆盖 Astro 配置:

typescript
export default getViteConfig( { test: { environment: 'node' } }, { site: 'https://example.com/', trailingSlash: 'always' } );

测试 .astro 组件

.astro 组件是服务端渲染的,不能像 React 组件那样直接用 Testing Library 的 render 方法。Astro 提供了两种测试方案:

方案一:Container API

Container API 是 Astro 内置的实验性功能,可以在服务端渲染 .astro 组件并返回 HTML 字符串:

typescript
// src/components/__tests__/Card.test.ts import { describe, it, expect } from 'vitest'; import { Container } from 'astro:container'; import Card from '../Card.astro'; describe('Card 组件', () => { it('渲染标题文本', async () => { const container = await Container.create(); const result = await container.renderToString(Card, { props: { title: 'Hello Astro' }, }); expect(result).toContain('Hello Astro'); }); });

方案二:vitest-browser-astro

vitest-browser-astro 将组件渲染到真实浏览器 DOM 中,支持更丰富的交互断言:

typescript
// vitest.config.ts 中添加插件 import { getViteConfig } from 'astro/config'; import { astroRenderer } from 'vitest-browser-astro/plugin'; import { playwright } from '@vitest/browser-playwright'; export default getViteConfig({ plugins: [astroRenderer()], test: { browser: { enabled: true, instances: [{ browser: 'chromium' }], provider: playwright(), headless: true, }, }, });
typescript
// src/components/__tests__/Card.test.ts import { render } from 'vitest-browser-astro'; import { expect, test } from 'vitest'; import Card from '../Card.astro'; test('渲染卡片标题', async () => { const screen = await render(Card, { props: { title: 'Hello Astro' }, }); await expect.element(screen.getByText('Hello Astro')).toBeVisible(); });

如果组件中嵌套了 React 或 Vue 框架组件,需要在配置中注册对应的 container renderer:

typescript
import { getContainerRenderer } from '@astrojs/react'; const container = await Container.create({ renderers: [getContainerRenderer()], });

注意:同一个配置中只能使用一种 JSX 框架 renderer,Vue 和 Svelte 等非 JSX 框架可以与 JSX 框架共存。

测试框架组件

React 组件测试

typescript
// src/components/__tests__/Counter.test.tsx // @vitest-environment jsdom import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Counter from '../Counter'; describe('Counter', () => { it('显示初始计数值', () => { render(<Counter initialCount={0} />); expect(screen.getByText('Count: 0')).toBeInTheDocument(); }); it('点击按钮后计数值加一', async () => { const user = userEvent.setup(); render(<Counter initialCount={0} />); await user.click(screen.getByRole('button', { name: 'Increment' })); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); });

Vue 组件测试

typescript
// src/components/__tests__/TodoList.test.ts // @vitest-environment jsdom import { describe, it, expect } from 'vitest'; import { mount } from '@vue/test-utils'; import TodoList from '../TodoList.vue'; describe('TodoList', () => { it('渲染待办事项列表', () => { 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('勾选完成状态后触发事件', 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(); }); });

测试 API 路由

Astro 的 API 路由导出 GETPOST 等函数,可以直接在测试中调用:

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 返回用户列表', 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(Array.isArray(data.users)).toBe(true); }); it('POST 创建新用户', async () => { const request = new Request('http://localhost/api/users', { method: 'POST', body: JSON.stringify({ name: 'John', email: 'john@example.com' }), }); const response = await POST({ request } as any); const data = await response.json(); expect(response.status).toBe(201); expect(data.name).toBe('John'); }); });

测试中间件

Astro 中间件的 onRequest 函数可以像普通函数一样测试,关键是构造正确的 context 对象:

typescript
// src/middleware/__tests__/auth.test.ts import { describe, it, expect, vi } from 'vitest'; import { onRequest } from '../auth'; describe('认证中间件', () => { it('无 token 时重定向到登录页', async () => { const request = new Request('http://localhost/dashboard'); const redirect = vi.fn(); await onRequest({ request, redirect } as any); expect(redirect).toHaveBeenCalledWith('/login'); }); it('有效 token 时放行请求', async () => { const request = new Request('http://localhost/dashboard', { headers: { Authorization: 'Bearer valid-token' }, }); const next = vi.fn().mockResolvedValue(new Response()); await onRequest({ request, next } as any); expect(next).toHaveBeenCalled(); }); });

端到端测试(Playwright)

Playwright 测试在真实浏览器中运行,需要先启动开发服务器。在 playwright.config.ts 中配置 webServer

typescript
// playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ webServer: { command: 'npm run dev', port: 4321, reuseExistingServer: !process.env.CI, }, testDir: './e2e', });

编写端到端测试:

typescript
// e2e/navigation.spec.ts import { test, expect } from '@playwright/test'; test.describe('页面导航', () => { test('首页加载正常', async ({ page }) => { await page.goto('/'); await expect(page).toHaveTitle(/My Astro App/); await expect(page.locator('h1')).toContainText('Welcome'); }); test('导航到关于页', async ({ page }) => { await page.goto('/'); await page.click('text=About'); await expect(page).toHaveURL(/\/about/); }); test('表单提交成功', async ({ page }) => { await page.goto('/contact'); await page.fill('input[name="name"]', 'John'); 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(); }); });

测试覆盖率

在 vitest.config.ts 中配置覆盖率报告:

typescript
// vitest.config.ts import { getViteConfig } from 'astro/config'; export default getViteConfig({ test: { coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/', 'src/test/', '**/*.d.ts', '**/*.config.*', ], }, }, });

npm scripts 配置

在 package.json 中添加测试命令:

json
{ "scripts": { "test": "vitest", "test:run": "vitest run", "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", "test:e2e:headed": "playwright test --headed" } }

测试策略建议

分层测试金字塔:底层大量单元测试覆盖工具函数和独立组件,中间适量集成测试验证组件协作和 API 逻辑,顶层少量端到端测试保障核心业务流程。

.astro 组件测试优先用 Container API:简单场景用 renderToString 断言 HTML 内容即可,需要交互断言时再用 vitest-browser-astro。

Mock 外部依赖:用 vi.mock() 隔离第三方模块和 API 调用,确保测试稳定且不依赖外部服务。

CI 中强制执行:在 CI 流程中运行 vitest run 和 playwright test,并设置覆盖率阈值,低于阈值则构建失败。

标签:Astro