Storybook 组件测试:使用 preview 文件的 beforeAll 钩子实现一次性项目级初始化与清理
本篇技术指南聚焦 Storybook 提供的测试级生命周期钩子 beforeAll,讲解如何在 .storybook/preview.* 预览配置文件中,为整个项目注册“只执行一次”的异步初始化逻辑(如启动项目引导 init()),并通过返回 cleanup 清理函数完成测试运行结束或重跑前的收尾。读完本文,你将掌握 beforeAll 的适用场景、在 CSF 3 与 CSF Next(definePreview)两种写法下的完整配置范式、与 beforeEach 的分工差异,以及其在源码中的实现机制与组合清理语义,可直接用于组件交互测试的工程化落地。
背景:为什么组件测试需要“项目级一次性初始化”
在 交互测试指南 中,Storybook 建议在为组件编写基于 play 函数的交互测试时,重视测试之间的状态隔离。当你对某个组件的状态做了修改(例如修改了 mock 数据、注入了 module mock),必须在渲染下一个 story 之前把状态还原,否则多个 story 之间会相互污染,导致断言结果不稳定。
针对“何时重置状态”,Storybook 在 preview 文件(.storybook/preview.*)中提供了两个项目级钩子:
beforeAll:在整个项目任何 story 执行之前只运行一次;beforeEach:在每个 story 执行之前运行。
本文的主角 beforeAll,其配套代码片段即仓库中的 docs/_snippets/before-all-in-preview.md,被正文文档引用在 docs/writing-tests/interaction-testing.mdx 的 “Set up or reset state for all tests → beforeAll” 小节。接下来我们将围绕该片段逐层展开。
beforeAll 的执行语义
按官方文档描述,preview 文件中的 beforeAll 函数具备如下特征:
- 它会在项目中任何 story 开始之前运行一次;
- 不会在 story 与 story 之间重复执行;
- 除了测试运行启动时的首次执行外,只有当 preview 文件本身被更新时才会再次运行(例如本地开发触发 HMR);
- 它是承载“整个项目都依赖的启动/引导逻辑”的推荐位置。
因此,当你的项目需要在跑测试前完成全局引导——例如执行 project-bootstrap 的 init()(初始化设计系统、连接测试桩环境、注册全局模块等)——beforeAll 是合适的位置。
支持返回 cleanup 清理函数
beforeAll 还支持返回一个异步清理函数,该清理函数会在以下两种时机执行:
- 在
beforeAll即将被重新运行之前(例如 preview 文件更新触发重新初始化时); - 在测试运行器(test runner)执行测试收尾(teardown)的过程中。
这为“启动时建立、结束时释放”的对称生命周期管理提供了官方支持。关于它的实现细节,见下文“源码视角”一节。
完整配置示例:在 preview 文件中注册 beforeAll
代码片段 before-all-in-preview.md 给出了两种编写体系的多个变体:经典 CSF 3 下通过默认导出的 Preview 对象声明 beforeAll;以及较新的 CSF Next(实验性)体系下通过 definePreview({...}) 声明。两者的钩子体完全一致,差异仅在导入源与配置形态。
1. CSF 3:Preview 默认导出(适用于任意 renderer)
文件:.storybook/preview.js 或 .storybook/preview.jsx(JavaScript 写法)
import { init } from '../project-bootstrap';
export default {
async beforeAll() {
await init();
},
};
文件:.storybook/preview.ts 或 .storybook/preview.tsx(TypeScript 写法)
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Preview } from '@storybook/your-framework';
import { init } from '../project-bootstrap';
const preview: Preview = {
async beforeAll() {
await init();
},
};
export default preview;
说明:代码中的
@storybook/your-framework为占位符,需按项目实际框架替换,例如react-vite、nextjs、vue3-vite等;../project-bootstrap是示例引导模块路径,应替换为你真实的引导模块。这里的beforeAll被声明为async,以支持在钩子内await异步初始化(例如等待网络、文件系统或子进程就绪)。
2. CSF Next(🧪 实验性):definePreview 声明式配置
CSF Next 体系通过 definePreview 获得类型提示更强的配置对象。以下分别给出 React、Vue、Angular、Web Components 各 renderer 的 TS/JS 变体。
React
文件:.storybook/preview.tsx(TypeScript)
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
import { definePreview } from '@storybook/your-framework';
import { init } from '../project-bootstrap';
export default definePreview({
async beforeAll() {
await init();
},
});
文件:.storybook/preview.jsx(JavaScript)
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
import { definePreview } from '@storybook/your-framework';
import { init } from '../project-bootstrap';
export default definePreview({
async beforeAll() {
await init();
},
});
Vue(vue3-vite)
文件:.storybook/preview.ts(TypeScript)
import { definePreview } from '@storybook/vue3-vite';
import { init } from '../project-bootstrap';
export default definePreview({
async beforeAll() {
await init();
},
});
文件:.storybook/preview.js(JavaScript)
import { definePreview } from '@storybook/vue3-vite';
import { init } from '../project-bootstrap';
export default definePreview({
async beforeAll() {
await init();
},
});
Angular
文件:.storybook/preview.ts(TypeScript)
import { definePreview } from '@storybook/angular';
import { init } from '../project-bootstrap';
export default definePreview({
async beforeAll() {
await init();
},
});
Web Components(web-components-vite)
文件:.storybook/preview.ts(TypeScript)
import { definePreview } from '@storybook/web-components-vite';
import { init } from '../project-bootstrap';
export default definePreview({
async beforeAll() {
await init();
},
});
文件:.storybook/preview.js(JavaScript)
import { definePreview } from '@storybook/web-components-vite';
import { init } from '../project-bootstrap';
export default definePreview({
async beforeAll() {
await init();
},
});
可以看到,无论采用哪种 renderer 或写法,beforeAll 钩子的用法都保持一致——这得益于 Storybook 将生命周期钩子统一抽象到了 preview 配置层。
何时用 beforeAll,何时改用 beforeEach
beforeAll 并不是唯一的选择。官方文档明确指出:重置组件与模块状态这类高频操作,通常应放到 preview 文件的 beforeAll / beforeEach 中,使其覆盖整个项目;仅当某个组件的诉求足够特殊时,才使用组件级 meta 的 beforeEach(见 before-each-in-meta-mock-date.md 及其对应的交互测试文档小节)。
两者的职责边界可以这样划分:
| 维度 | preview 中的 beforeAll |
preview 中的 beforeEach |
|---|---|---|
| 执行频率 | 整个项目测试开始前仅一次 | 项目内每个 story 之前都执行 |
| 典型用途 | 一次性引导(如 init() 启动引导、连接测试环境) |
重置被 mock 的状态或模块(如重置被 mock 的 Date) |
| 再次触发的条件 | preview 文件被更新才会重跑 | 每渲染一个 story 都会执行 |
| cleanup | 支持返回清理函数,在重跑前或 test runner 收尾时执行 | 支持返回清理函数,在每次 story 结束/切换后执行 |
配套的 beforeEach 示例见 before-each-in-preview.md,例如在 beforeEach 中调用 MockDate.reset():
import MockDate from 'mockdate';
export default {
async beforeEach() {
MockDate.reset();
},
};
如何选择:如果初始化成本高、且只需做一次(例如启动本地 mock server、加载全局字体或图标、注册全局 decorator 依赖),放在 beforeAll;如果需要保证每个 story 之间的状态互不泄漏,则依赖每次运行的 beforeEach(或 beforeEach 返回的清理函数)。请勿把“逐 story 重置”的任务委托给只执行一次的 beforeAll——那会造成 story 间的状态串扰。
官方文档还特别提醒:无需手动还原 fn() mock,Storybook 会在渲染每个 story 之前自动完成恢复(参见 parameters.test.restoreMocks 相关说明),因此 beforeAll / beforeEach 无需操心这部分工作。
源码视角:BeforeAll 的类型定义与组合机制
为了让 beforeAll 的语义更可验证,我们回到实现层面对照仓库源码。
钩子类型定义
在 code/core/src/csf/story.ts 中可以找到 BeforeAll 与 CleanupCallback 的类型定义:
export type CleanupCallback = () => Awaitable<unknown>;
export type BeforeAll = () => Awaitable<CleanupCallback | void>;
可见 BeforeAll 是一个可返回 CleanupCallback 或 void 的(可)异步函数——这正是文档中“可以返回清理函数”一说的类型基础。该类型在 preview 配置中被组合进 beforeAll 字段,类型约束位置见 code/core/src/types/modules/story.ts。
多配置源如何合并:composeBeforeAllHooks
Storybook 允许项目的多个配置文件(如不同 addon 或组合配置)各自声明 beforeAll。合并逻辑位于 composeBeforeAllHooks,它按顺序依次执行所有钩子,收集每个钩子返回的 cleanup,然后返回一个“按逆序执行所有 cleanup”的函数:
export const composeBeforeAllHooks = (hooks: BeforeAll[]): BeforeAll => {
return async () => {
const cleanups: CleanupCallback[] = [];
for (const hook of hooks) {
const cleanup = await hook();
if (cleanup) {
cleanups.unshift(cleanup);
}
}
return async () => {
for (const cleanup of cleanups) {
await cleanup();
}
};
};
};
这段实现揭示了三个重要语义:
- 多个
beforeAll钩子按声明顺序依次await执行,即前一个完成才轮到后一个; - 每个钩子返回的清理函数被收集起来;
- 最终返回的组合清理函数会以逆序(LIFO)执行所有 cleanup,类似“栈式释放”。
对应的单元测试 code/core/src/preview-api/modules/store/csf/beforeAll.test.ts 对上述行为做了完整验证:包括普通钩子按序执行、cleanup 按逆序执行(期望顺序为 three cleanup → two cleanup → one cleanup)、异步钩子与异步 cleanup 同样遵循“按序 setup、逆序 teardown”的语义。此外 composeConfigs.ts 会将各配置模块中的 beforeAll 字段提取并合并进最终的项目注解(project annotations),印证了“preview 文件中声明的 beforeAll 会进入全局配置管线”这一事实。
在 Portable Stories / Vitest 场景中编排 beforeAll
beforeAll 不仅作用于 Storybook 内建的交互测试运行,也会随项目注解一起暴露给外部测试框架。仓库的 CSF Next 文档 docs/api/csf/csf-next.mdx 展示了在 Vitest 环境下如何把组合后的 preview 注解接入 Vitest 生命周期:将 preview.composed.beforeAll 注册为 Vitest 的全局 beforeAll(源码上下文见该文档中 beforeAll(preview.composed.beforeAll) 与 beforeAll(annotations.beforeAll) 的迁移对照)。也就是说:
- 在 Portable Stories(在 Vitest/Jest 中组合并运行 story)的工程里,你在
.storybook/preview.*中声明的beforeAll会被作为“项目注解”传递给测试框架,由 Vitest 的beforeAll生命周期调度执行; - 而 CSF Next 的
definePreview组合产物与经典 CSF 3 的 default export 在这一点上是等价的,都遵循相同的组合与合并规则(可参见 portable-stories.test.ts 中对“composed beforeAll 作为项目注解一部分返回”的断言)。
最佳实践与注意事项
结合上述文档与源码,在项目中使用 preview 的 beforeAll 时建议遵循以下实践:
- 只放“真正一次性”的全局初始化。例如执行
project-bootstrap引导、启动全局测试服务、注入一次性环境变量等;频率更高的状态重置请交给beforeEach。 - 善用返回的 cleanup 函数做对称收尾。官方语义是“重跑前 / test runner 收尾时执行”,适用于释放资源、停止临时服务、恢复全局副作用。其逆序执行特性(源自 beforeAll.ts)意味着依赖关系上“后建立的先释放”,符合常规资源管理预期。
- 保持钩子幂等。由于 preview 文件更新会触发
beforeAll重跑,若初始化逻辑本身有副作用(如重复注册、重复启动),应在 cleanup 中做好释放,保证“再次初始化”是安全、可重复的。 - 理解多配置源的组合顺序。当项目存在多个来源的
beforeAll(例如内置服务注册与用户自定义逻辑并存)时,它们会依序执行、cleanup 逆序执行,因此不要在某个钩子中假设自己是“唯一”的初始化入口。 - 不必为
fn()mock 写还原逻辑。Storybook 会在渲染 story 前自动恢复 mock,beforeAll无需承担此职责。
参考
- 本文配套代码片段:docs/_snippets/before-all-in-preview.md
- 正文上下文:docs/writing-tests/interaction-testing.mdx(“Set up or reset state for all tests”小节)
- 对照用
beforeEach片段:docs/_snippets/before-each-in-preview.md - 类型定义:code/core/src/csf/story.ts
- 组合实现与测试:code/core/src/preview-api/modules/store/csf/beforeAll.ts、beforeAll.test.ts
- Portable Stories / Vitest 场景:docs/api/portable-stories/portable-stories-vitest.mdx、docs/api/csf/csf-next.mdx
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 StartedRust0626
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