首页
/ Storybook 组件测试:使用 preview 文件的 beforeAll 钩子实现一次性项目级初始化与清理

Storybook 组件测试:使用 preview 文件的 beforeAll 钩子实现一次性项目级初始化与清理

2026-09-07 09:07:37作者:郦嵘贵Just

本篇技术指南聚焦 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-bootstrapinit()(初始化设计系统、连接测试桩环境、注册全局模块等)——beforeAll 是合适的位置。

支持返回 cleanup 清理函数

beforeAll 还支持返回一个异步清理函数,该清理函数会在以下两种时机执行:

  1. beforeAll 即将被重新运行之前(例如 preview 文件更新触发重新初始化时);
  2. 在测试运行器(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-vitenextjsvue3-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 中可以找到 BeforeAllCleanupCallback 的类型定义:

export type CleanupCallback = () => Awaitable<unknown>;

export type BeforeAll = () => Awaitable<CleanupCallback | void>;

可见 BeforeAll 是一个可返回 CleanupCallbackvoid 的(可)异步函数——这正是文档中“可以返回清理函数”一说的类型基础。该类型在 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();
      }
    };
  };
};

这段实现揭示了三个重要语义:

  1. 多个 beforeAll 钩子按声明顺序依次 await 执行,即前一个完成才轮到后一个;
  2. 每个钩子返回的清理函数被收集起来;
  3. 最终返回的组合清理函数会以逆序(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 时建议遵循以下实践:

  1. 只放“真正一次性”的全局初始化。例如执行 project-bootstrap 引导、启动全局测试服务、注入一次性环境变量等;频率更高的状态重置请交给 beforeEach
  2. 善用返回的 cleanup 函数做对称收尾。官方语义是“重跑前 / test runner 收尾时执行”,适用于释放资源、停止临时服务、恢复全局副作用。其逆序执行特性(源自 beforeAll.ts)意味着依赖关系上“后建立的先释放”,符合常规资源管理预期。
  3. 保持钩子幂等。由于 preview 文件更新会触发 beforeAll 重跑,若初始化逻辑本身有副作用(如重复注册、重复启动),应在 cleanup 中做好释放,保证“再次初始化”是安全、可重复的。
  4. 理解多配置源的组合顺序。当项目存在多个来源的 beforeAll(例如内置服务注册与用户自定义逻辑并存)时,它们会依序执行、cleanup 逆序执行,因此不要在某个钩子中假设自己是“唯一”的初始化入口。
  5. 不必为 fn() mock 写还原逻辑。Storybook 会在渲染 story 前自动恢复 mock,beforeAll 无需承担此职责。

参考

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