首页
/ Astro Container API + Vitest:对组件、React Island 与动态路由进行单元测试的官方示例详解

Astro Container API + Vitest:对组件、React Island 与动态路由进行单元测试的官方示例详解

2026-09-04 12:27:20作者:姚月梅Lane

Astro 官方仓库内置了一个专门演示「用 Vitest 测试 Astro 项目」的示例工程(examples/container-with-vitest),其核心思路是利用 Container API 在测试运行时直接渲染 .astro 组件,而不需要启动完整的开发服务器或构建产物。读完本文,你将掌握 vitest.config.ts 中基于 getViteConfig 的环境搭建方式、renderToStringslots / 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 中的文件布局如下,三个测试文件分别对应三类典型测试场景:

依赖与运行环境(见 package.json):

  • Node 版本要求:"node": ">=22.12.0"
  • 测试脚本:"test": "vitest run"(一次性运行,非 watch 模式)
  • 依赖:astrovitest@astrojs/reactreact / 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 的实际工作过程:

  1. 返回一个异步的 Vite 配置函数,从中解构出 modecommand;由于 Vite 的 commandserve | build 而 Astro 用 dev | build,内部先做了 serve → dev 的映射;
  2. 动态导入 viteresolveConfig / createSettingscreateVite、集成 hooks 等模块——注释明确说明「用动态 import 避免在不使用时引入依赖」;
  3. 通过 resolveConfig + runHookConfigSetup 解析 Astro 配置(即读取项目中的 astro.config.ts,本例中的 React 集成就是这样被激活的);
  4. createRoutesList 扫描路由,再用 createVite 生成 Astro 的 Vite 配置;
  5. 最后 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)支持 streamingmanifestrenderersresolveastroConfig 等选项,其中 renderers 用于注入客户端框架(React、Vue 等)的容器渲染器,manifest 则允许复用真实应用的渲染清单。
  • renderToString(component, options):把组件渲染成 HTML 字符串。源码实现(L535-L545)非常简洁——先把 slots 中的字符串值统一标记为「slot 字符串」,然后委托给 renderToResponse 并取 response.text()

renderToResponseL566-L598)揭示了容器的本质:它把一次「组件渲染」模拟成一次真实的请求处理——构造默认 Request(缺省为 https://example.com/)、插入 RouteData 路由条目、创建 FetchState,然后走 handleMiddleware(state, handlePages) 的标准页面处理链。这也解释了为什么渲染选项里可以传 requestparamsContainerRenderOptions 的主要字段有:

选项 作用 示例中的使用
slots 以字符串或已渲染 HTML 填充组件的 <slot> Card 的默认插槽
props 传入组件 frontmatter 解构的 Astro.props CounterLightcount
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);
});

两个值得注意的写法:

  1. 插槽可以直接传字符串。第一个用例把 'Card content' 作为默认插槽内容传入,断言渲染结果同时包含组件自身的 'This is a card' 和插槽内容——这正是 renderToStringmarkAllSlotsAsSlotString 所处理的场景。
  2. 可以「先渲染子组件、再把 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"',
  );
});

三个要点:

  1. loadRenderers 来自虚拟模块 astro:containergetContainerRenderer 来自 @astrojs/react/container-renderer——即 React 集成专门暴露给容器环境的渲染器入口;
  2. 模块顶层就执行了 await loadRenderers(顶层 await),容器在模块加载时一次性创建好,所有用例共享;
  3. 断言验证了 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 上下文:容器会用它解析路径(源码中 renderToResponseoptions?.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.jsonengines 声明),并在 vitest.config.ts 中通过 getViteConfig 复用 Astro 的 Vite 管线;
  • 能测什么.astro 组件的静态渲染输出、插槽组合、prop 传递、动态路由参数、客户端框架组件的 SSR HTML 与 hydration 元数据,甚至 endpoint(routeType: 'endpoint');
  • 容器做了什么:从 packages/astro/src/container/index.tsrenderToResponse 实现看,容器把组件渲染包装进真实的路由/中间件处理链(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)。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
901
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
589
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341