Docs
Storybook Docs

快照测试

快照测试

¥Snapshot tests

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

¥Snapshot testing is simply rendering a component in a given state, taking a snapshot of the rendered DOM or HTML, and then comparing it against the previous snapshot. They’re convenient to create, but can be difficult and noisy to maintain if the snapshot contains too much information. For UI components, visual tests (easier to review) or interaction tests (focused on functionality) are usually the better fit. However, there are some cases where snapshot testing may be necessary, such as ensuring an error is thrown correctly.

你可以将你的故事重用为其他测试环境(例如 Jest 或 Vitest)中快照测试的基础。为了启用此功能,Storybook 提供了可移植故事 API,该 API 可以使用你的故事及其注释(argsdecorators参数 等)编写故事,并为你的测试生成可渲染的元素。可移植故事适用于:

¥You can reuse your stories as the basis of snapshot tests within another test environment, like Jest or Vitest. To enable this, Storybook provides the Portable Stories API, which composes your stories with their annotations (args, decorators, parameters, etc) and produces a renderable element for your tests. Portable Stories are available for:

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

¥Looking for snapshot testing with Storyshots? Storyshots is deprecated and no longer maintained. We recommend using the Portable Stories API instead.

有关如何迁移测试的更多信息,请参考 Storyshots 文档

¥Please reference the Storyshots documentation for more information on how to migrate your tests.

开始使用可移植故事

¥Get started with Portable Stories

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

¥If you’re using Storybook Test, your project is already configured to use Portable Stories in Vitest.

如果你没有使用 Storybook Test 或想在其他测试环境中进行测试,请遵循相关文档:

¥If you’re not using Storybook Test or would like to test in another testing environment, please follow the relevant documentation:

对可移植故事进行快照测试

¥Snapshot testing a portable story

对可复用故事进行快照测试是一个简单的过程,使用 Portable Stories API 中的 composeStories 获取可渲染元素,渲染该元素,然后拍摄并比较快照。

¥Snapshot testing a reusable story is a straightforward process of using composeStories from the Portable Stories API to get a renderable element, rendering that element, and then taking and comparing a snapshot.

此示例在 Vitest 中渲染一个 Button 组件(通过复用 Button 的某个故事),并断言渲染的 HTML 快照与原始代码匹配。

¥This example renders a Button component in Vitest (by reusing one of Button’s stories) and asserts that the rendered HTML snapshot matches.

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();
});

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

¥Once the test has run, a snapshot will be inserted or created. Then, when you run tests again and the snapshot doesn’t match, the test will fail and you will see output something like this:

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)

¥How long did it take you to find what changed? (px-4px-3)

这正是 可视化测试 在测试 UI 组件外观方面更胜一筹的原因。不仅可以立即看到更改的内容,还可以测试用户将看到的实际外观,而不仅仅是应用的 CSS。

¥This is exactly why visual tests are so much better for testing the appearance of UI components. Not only is it immediately apparent what has changed, but it also tests the actual appearance your users will see, not merely the CSS applied.

验证是否抛出错误

¥Verifying an error is thrown

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

¥Now that we know how to do general snapshot testing, let’s apply it to a common use case: verifying that an expected error is thrown correctly.

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

¥In this example, we have a simple Button React component which for some reason accepts a prop, doNotUseThisItWillThrowAnError, which will (unsurprisingly) throw an error if it is used.

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

然后,我们会有一个通过 args 应用该属性的故事。它还会删除默认的 devtest 标签,以防止故事显示在 Storybook 侧边栏中,以及被 Storybook Test 测试为故事。

¥We then have a story which applies that prop via args. It also removes the default dev and test tags to prevent the story from displaying in the Storybook sidebar and from being tested as a story (by Storybook Test), respectively.

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

最后,我们在测试文件中编写一个测试,断言抛出错误并显示特定消息。

¥Finally, we write a test in the test file which asserts that an error is thrown with a particular message.

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...');
});

此示例已简化,仅供学习参考。相同的技术可以应用于更复杂的场景,例如包含无效输入的表单或模拟网络故障。

¥This example is simplified for educational purposes. The same technique could be applied to more complex scenarios, such as a form with invalid input or a simulated network failure.

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

¥Snapshot testing with the test-runner

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

¥If you cannot use portable stories in your project, you can still run snapshot tests using the test-runner. Follow the instructions in the test-runner documentation to set up the test-runner with snapshot tests in your project.

常见问题

¥FAQ

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

¥What’s the difference between snapshot tests and visual tests?

Visual Tests 捕获故事图片并将其与图片基线进行比较。快照测试会截取 DOM 或 HTML 快照,并将其与 DOM 或 HTML 基准进行比较。Visual Tests 更适合验证外观。快照测试对于验证非可视化输出并确保 DOM 不变非常有用。

¥Visual tests capture images of stories and compare them against image baselines. Snapshot tests take DOM or HTML snapshots and compare them against DOM or HTML baselines. Visual tests are better suited for verifying appearance. Snapshot tests are useful for validating non-visual output and ensuring the DOM doesn’t change.

更多测试资源

¥More testing resources