Astro Container API + Vitest:对组件、React Island 与动态路由进行单元测试的官方示例详解
Astro 官方仓库内置了一个专门演示「用 Vitest 测试 Astro 项目」的示例工程(examples/container-with-vitest),其核心思路是利用 Container API 在测试运行时直接渲染 .astro 组件,而不需要启动完整的开发服务器或构建产物。读完本文,你将掌握 vitest.config.ts 中基于 getViteConfig 的环境搭建方式、renderToString 的 slots / props / params / request 等渲染选项,以及如何在纯 Node 环境中测试带 React client 指令的组件。
一、示例工程:定位、创建方式与目录结构
官方 README 对该示例的定位是一句话:用 Vitest + Container API 来测试 Astro 组件(见 examples/container-with-vitest/README.md)。它提供了标准的模板创建命令:
npm create astro@latest -- --template container-with-vitest
该示例工程在 monorepo 中的文件布局如下,三个测试文件分别对应三类典型测试场景:
- test/Card.test.ts —— 纯 Astro 组件:插槽(slot)与嵌套组件渲染
- test/ReactWrapper.test.ts —— 客户端框架组件(React)的 SSR 渲染与 hydration 标记
- test/[locale].test.ts —— 动态路由页面:模拟路由参数与
Request - vitest.config.ts —— 关键配置:让 Vitest 复用 Astro 的 Vite 管线
- src/components/Card.astro、src/components/CounterLight.astro、src/components/ReactWrapper.astro、src/pages/[locale].astro —— 被测试的组件与页面
依赖与运行环境(见 package.json):
- Node 版本要求:
"node": ">=22.12.0" - 测试脚本:
"test": "vitest run"(一次性运行,非 watch 模式) - 依赖:
astro、vitest、@astrojs/react、react/react-dom
astro.config.ts 中注册了 React 集成(integrations: [react()]),tsconfig.json 则继承 astro/tsconfigs/strict 并把 **/* 纳入类型检查范围——测试文件本身也受严格类型约束。
二、测试环境搭建的关键:用 getViteConfig 打通 .astro 模块
整个示例最重要的一行配置在 vitest.config.ts:
/// <reference types="vitest/config" />
import { getViteConfig } from 'astro/config';
export default getViteConfig({
test: {
/* for example, use global to avoid globals imports (describe, test, expect): */
// globals: true,
},
});
它把「Vitest 的 test 配置」交给 astro/config 导出的 getViteConfig 去包裹。为什么这一步必不可少?因为 .astro 文件不是 Vite 默认认识的模块,必须经过 Astro 的 Vite 插件(@astrojs/vite-plugin-astro 编译管线)才能被 import。getViteConfig 的作用就是:从 Astro 配置出发生成一份完整解析后的 Vite 配置,再与用户传入的配置(这里包含 test 段)合并,使 Vitest 启动的 Vite 环境天然具备编译 .astro 的能力。
从 packages/astro/src/config/index.ts 的实现可以看到 getViteConfig 的实际工作过程:
- 返回一个异步的 Vite 配置函数,从中解构出
mode和command;由于 Vite 的command是serve | build而 Astro 用dev | build,内部先做了serve → dev的映射; - 动态导入
vite、resolveConfig/createSettings、createVite、集成 hooks 等模块——注释明确说明「用动态 import 避免在不使用时引入依赖」; - 通过
resolveConfig+runHookConfigSetup解析 Astro 配置(即读取项目中的astro.config.ts,本例中的 React 集成就是这样被激活的); - 用
createRoutesList扫描路由,再用createVite生成 Astro 的 Vite 配置; - 最后
mergeConfig(viteConfig, userViteConfig)把用户传入的test配置合并进去返回。
也就是说,Vitest 拿到的是一份「Astro 完整管线 + 你的 test 选项」的配置,test/*.test.ts 里可以直接 import Card from '../src/components/Card.astro'。
三、Container API 基础:create() 与 renderToString()
三个测试文件都从 astro/container 引入容器:
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
其实现位于 packages/astro/src/container/index.ts。从源码结构看,容器的核心方法有两个:
AstroContainer.create(containerOptions):创建容器实例。create()的参数(见源码 L363-L370)支持streaming、manifest、renderers、resolve、astroConfig等选项,其中renderers用于注入客户端框架(React、Vue 等)的容器渲染器,manifest则允许复用真实应用的渲染清单。renderToString(component, options):把组件渲染成 HTML 字符串。源码实现(L535-L545)非常简洁——先把slots中的字符串值统一标记为「slot 字符串」,然后委托给renderToResponse并取response.text()。
而 renderToResponse(L566-L598)揭示了容器的本质:它把一次「组件渲染」模拟成一次真实的请求处理——构造默认 Request(缺省为 https://example.com/)、插入 RouteData 路由条目、创建 FetchState,然后走 handleMiddleware(state, handlePages) 的标准页面处理链。这也解释了为什么渲染选项里可以传 request 和 params。ContainerRenderOptions 的主要字段有:
| 选项 | 作用 | 示例中的使用 |
|---|---|---|
slots |
以字符串或已渲染 HTML 填充组件的 <slot> |
Card 的默认插槽 |
props |
传入组件 frontmatter 解构的 Astro.props |
CounterLight 的 count |
params |
模拟动态路由参数(Astro.params) |
[locale] 页面的 locale: 'en' |
request |
自定义 Request,控制 URL 与请求上下文 |
new Request('http://example.com/en') |
locals |
注入 Astro.locals |
示例未使用 |
routeType |
'page' 或 'endpoint',默认 page |
示例未使用(api.ts 为 endpoint 页面) |
partial |
是否按部分渲染处理,默认 true |
示例未使用 |
streaming / renderers / manifest / resolve / astroConfig |
create() 阶段的容器级选项 |
renderers 用于 React 测试 |
四、实战场景一:测试插槽与嵌套 Astro 组件
Card.astro 是一个带默认插槽的组件,CounterLight.astro 是接收 count prop 的组件。对应的 Card.test.ts 覆盖了两类用例:
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import Card from '../src/components/Card.astro';
import CounterLight from '../src/components/CounterLight.astro';
test('Card with slots', async () => {
const container = await AstroContainer.create();
const result = await container.renderToString(Card, {
slots: {
default: 'Card content',
},
});
expect(result).toContain('This is a card');
expect(result).toContain('Card content');
});
test('Card with nested CounterLight', async () => {
const container = await AstroContainer.create();
const counterLight = await container.renderToString(CounterLight, { props: { count: 1 } });
const result = await container.renderToString(Card, {
slots: {
default: counterLight,
},
});
expect(result).toContain('This is a card');
expect(result).toContain(counterLight);
});
两个值得注意的写法:
- 插槽可以直接传字符串。第一个用例把
'Card content'作为默认插槽内容传入,断言渲染结果同时包含组件自身的'This is a card'和插槽内容——这正是renderToString中markAllSlotsAsSlotString所处理的场景。 - 可以「先渲染子组件、再把 HTML 作为插槽传给父组件」。第二个用例先单独渲染
CounterLight(通过props: { count: 1 }传参),再把得到的 HTML 字符串塞进Card的插槽,验证嵌套组合的完整性。这为测试「组件组合关系」提供了不依赖路由的手段。
五、实战场景二:测试 React 组件与 client 指令的 hydration 标记
当组件树里包含客户端框架组件(如 Counter.jsx 配合 <Counter initialCount={5} client:load /> 使用的 ReactWrapper.astro)时,需要向容器注入 React 的容器渲染器。ReactWrapper.test.ts 的完整写法是:
import { loadRenderers } from 'astro:container';
import { getContainerRenderer } from '@astrojs/react/container-renderer';
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import ReactWrapper from '../src/components/ReactWrapper.astro';
const renderers = await loadRenderers([getContainerRenderer()]);
const container = await AstroContainer.create({
renderers,
});
test('ReactWrapper with react renderer', async () => {
const result = await container.renderToString(ReactWrapper);
expect(result).toContain('Counter');
expect(result).toContain('Count: <!-- -->5');
expect(result, 'Includes client hydration reference').toContain(
'renderer-url="@astrojs/react/client.js"',
);
});
三个要点:
loadRenderers来自虚拟模块astro:container,getContainerRenderer来自@astrojs/react/container-renderer——即 React 集成专门暴露给容器环境的渲染器入口;- 模块顶层就执行了
await loadRenderers(顶层 await),容器在模块加载时一次性创建好,所有用例共享; - 断言验证了 SSR 输出的两个特征:
Count: <!-- -->5是 React SSR 在插值前后注入注释标记的典型形态,说明 React 确实走了服务端渲染路径;renderer-url="@astrojs/react/client.js"则证明client:load指令生成了客户端 hydration 所需的 island 元数据。这两条断言共同确认了「SSR HTML 正确 + 客户端加载引用正确」。
六、实战场景三:测试动态路由页面(params 与 request)
[locale].astro 是一个带 getStaticPaths() 的动态路由页面,frontmatter 中从 Astro.params 解构出 locale 并渲染为 <p>Locale: {locale}</p>。对应的 [locale].test.ts 展示了如何为动态路由构造渲染上下文:
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import Locale from '../src/pages/[locale].astro';
test('Dynamic route', async () => {
const container = await AstroContainer.create();
// @ts-ignore
const result = await container.renderToString(Locale, {
params: {
locale: 'en',
},
request: new Request('http://example.com/en'),
});
expect(result).toContain('Locale: en');
});
要点解析:
params对应Astro.params:不传params: { locale: 'en' },页面里的{locale}会是undefined;request对应 URL 上下文:容器会用它解析路径(源码中renderToResponse以options?.request ?? new Request('https://example.com/')取 URL),测试里传入http://example.com/en与路由参数保持一致;// @ts-ignore的原因:params的静态类型由该页面的getStaticPaths推导,而测试侧直接构造对象字面量时类型系统无法确认其合法性,示例工程选择用@ts-ignore绕过——这是在 strict 模式下使用 Container API 测试动态路由时的一个现实细节。
七、小结:这套测试方案的边界与适用前提
- 适用前提:Node
>=22.12.0(示例package.json的engines声明),并在vitest.config.ts中通过getViteConfig复用 Astro 的 Vite 管线; - 能测什么:
.astro组件的静态渲染输出、插槽组合、prop 传递、动态路由参数、客户端框架组件的 SSR HTML 与 hydration 元数据,甚至 endpoint(routeType: 'endpoint'); - 容器做了什么:从 packages/astro/src/container/index.ts 的
renderToResponse实现看,容器把组件渲染包装进真实的路由/中间件处理链(handleMiddleware(state, handlePages)),因此渲染结果与运行时行为高度一致; - 与项目自身单测的区分:Astro monorepo 内部包(如
packages/astro)的单元测试约定使用node:test,规则见 reference/unit-testing.md;本示例面向的是用户侧项目——在自己的 Astro 站点里用 Vitest 测试组件与页面。
参考入口:示例 README(examples/container-with-vitest/README.md)、容器 API 实现(packages/astro/src/container/index.ts)、getViteConfig 实现(packages/astro/src/config/index.ts)、容器内部单测(packages/astro/test/units/render/container.test.ts)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00