加入在线会议:美国东部时间周四上午 11 点,Storybook 9 版本发布及 AMA(你问我答)
文档
Storybook Docs

快照测试

快照测试

快照测试就是简单地在给定状态下渲染组件,捕获渲染后的 DOM 或 HTML 快照,然后与之前的快照进行比较。它们创建起来很方便,但如果快照包含太多信息,则维护起来可能很困难且冗余。对于 UI 组件,可视化测试(更容易审查)或交互测试(专注于功能)通常更合适。但是,在某些情况下,快照测试可能是必要的,例如确保正确抛出错误。

您可以将故事作为快照测试的基础,并在另一个测试环境中(如 Jest 或 Vitest)重用它们。为了实现这一点,Storybook 提供了可移植故事 API,它将您的故事及其注解(args(参数)decorators(装饰器)parameters(参数) 等)组合起来,并为您的测试生成一个可渲染的元素。可移植故事适用于

正在寻找使用 Storyshots 进行快照测试?Storyshots 已弃用且不再维护。我们建议改用可移植故事 API。

请参考Storyshots 文档,了解如何迁移测试的更多信息。

可移植故事入门

如果您正在使用 Storybook Test,您的项目已经配置为在 Vitest 中使用可移植故事。

如果您未使用 Storybook Test 或希望在其他测试环境中进行测试,请查阅相关文档

测试可移植故事的快照

测试可重用故事的快照是一个直接的过程,即使用可移植故事 API 中的 composeStories 获取可渲染元素,渲染该元素,然后捕获并比较快照。

此示例在 Vitest 中渲染了一个 Button 组件(通过重用 Button 的其中一个故事),并断言渲染的 HTML 快照匹配。

test/Button.test.js|ts
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import { composeStories } from '@storybook/your-framework';
 
import * as stories from '../stories/Button.stories';
 
const { Primary } = composeStories(stories);
test('Button snapshot', async () => {
  await Primary.run();
  expect(document.body.firstChild).toMatchSnapshot();
});

测试运行后,将插入或创建快照。然后,当您再次运行测试且快照不匹配时,测试将失败,您将看到类似以下的输出

FAIL  src/components/ui/Button.test.ts > Button snapshot
Error: Snapshot `Button snapshot 1` mismatched
 
- Expected
+ Received
 
  <div>
    <button
-     class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 h-9 px-4 py-2 has-[>svg]:px-3"
+     class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 h-9 px-3 py-2 has-[>svg]:px-3"
      data-slot="button"
    >
      Button
    </button>
  </div>

您花了多长时间找到更改之处?(px-4px-3)

这正是为什么可视化测试在测试 UI 组件外观方面如此优越的原因。它不仅能立即看出变化之处,还能测试用户实际看到的外观,而不仅仅是应用的 CSS。

验证是否抛出了错误

现在我们知道如何进行一般的快照测试,让我们将其应用于一个常见的用例:验证是否正确抛出了预期错误。

在此示例中,我们有一个简单的 React Button 组件,它由于某种原因接受一个 prop,doNotUseThisItWillThrowAnError,如果使用了它,就会(不出所料地)抛出错误。

Button.tsx
function Button(props) {
  if (props.doNotUseThisItWillThrowAnError) {
    throw new Error("I tried to tell you...")
  }
 
  return <button {...props} />
}

然后我们有一个故事,它通过 args 应用该 prop。它还移除了默认的 devtest 标签,分别用于防止故事显示在 Storybook 侧边栏中,以及防止 Storybook Test 将其作为故事进行测试。

Button.stories.js
export const ThrowError = {
  tags: ['!dev', '!test'],
  args: {
    doNotUseThisItWillThrowAnError: true,
  },
}

最后,我们在测试文件中编写了一个测试,断言会抛出带有特定消息的错误。

Button.test.ts
// @vitest-environment jsdom
 
import { expect, test } from "vitest";
 
import { composeStories } from "@storybook/react";
 
import * as stories from "./Button.stories";
 
const { ThrowError } = composeStories(stories);
 
test("Button throws error", async () => {
  await expect(ThrowError.run()).rejects.toThrowError('I tried to tell you...');
});

此示例为教育目的而简化。相同的技术可以应用于更复杂的场景,例如带有无效输入的表单或模拟网络故障。

使用测试运行器进行快照测试

如果您的项目中无法使用可移植故事,您仍然可以使用测试运行器运行快照测试。按照测试运行器文档中的说明,在您的项目中设置带有快照测试的测试运行器。

常见问题

快照测试和可视化测试有什么区别?

可视化测试捕获故事的图像并将其与图像基准进行比较。快照测试捕获 DOM 或 HTML 快照并将其与 DOM 或 HTML 基准进行比较。可视化测试更适合验证外观。快照测试对于验证非可视化输出和确保 DOM 不发生变化非常有用。

更多测试资源