Docs
Storybook Docs

快照测试

快照测试将每个故事的渲染标记与已知基线进行比较。这是一种识别触发渲染错误和警告的标记更改的方法。

¥Snapshot tests compare the rendered markup of every story against known baselines. It’s a way to identify markup changes that trigger rendering errors and warnings.

Storybook 是一个有用的快照测试工具,因为每个故事本质上都是一个测试规范。每次你编写或更新故事时,你都可以免费获得快照测试。

¥Storybook is a helpful tool for snapshot testing because every story is essentially a test specification. Any time you write or update a story, you get a snapshot test for free.

Example Snapshot test

如果你是 upgrading 到 Storybook 8.0 并且正在使用 Storyshots 插件进行快照测试,它已被正式弃用并在此版本中删除。有关更多信息,请参阅 迁移指南

¥If you're upgrading to Storybook 8.0 and were using the Storyshots addon for snapshot testing, it was officially deprecated and removed with this release. See the migration guide for more information.

使用测试运行器自动执行快照测试

¥Automate snapshot tests with the test-runner

Storybook 测试运行器将你的所有故事变成可执行测试。由 JestPlaywright 提供支持。它是一个独立的、与框架无关的实用程序,与你的 Storybook 并行运行。它使你能够在多浏览器环境中运行多种测试模式,包括使用 播放函数、DOM 快照和 可访问性测试 进行组件测试。

¥Storybook test-runner turns all of your stories into executable tests. Powered by Jest and Playwright. It's a standalone, framework-agnostic utility that runs parallel to your Storybook. It enables you to run multiple testing patterns in a multi-browser environment, including component testing with the play function, DOM snapshot, and accessibility testing.

设置

¥Setup

要使用测试运行器启用快照测试,你需要采取其他步骤来正确设置它。我们建议你在继续进行其余所需配置之前先完成 测试运行器文档,以了解有关可用选项和 API 的更多信息。

¥To enable snapshot testing with the test-runner, you'll need to take additional steps to set it up properly. We recommend you go through the test-runner documentation before proceeding with the rest of the required configuration to learn more about the available options and APIs.

在你的 Storybook 目录中添加一个新的 配置文件,其中包含以下内容:

¥Add a new configuration file inside your Storybook directory with the following inside:

.storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';
 
const config: TestRunnerConfig = {
  async postVisit(page, context) {
    // the #storybook-root element wraps the story. In Storybook 6.x, the selector is #root
    const elementHandler = await page.$('#storybook-root');
    const innerHTML = await elementHandler.innerHTML();
    expect(innerHTML).toMatchSnapshot();
  },
};
 
export default config;

postVisit 钩子允许你扩展测试运行器的默认配置。阅读有关它们 此处 的更多信息。

¥The postVisit hook allows you to extend the test runner's default configuration. Read more about them here.

当你执行测试运行器(例如,使用 yarn test-storybook)时,它将运行你的所有故事并运行快照测试,为位于 __snapshots__ 目录中的项目中的每个故事生成一个快照文件。

¥When you execute the test-runner (for example, with yarn test-storybook), it will run through all of your stories and run the snapshot tests, generating a snapshot file for each story in your project located in the __snapshots__ directory.

配置

¥Configure

开箱即用,测试运行器提供了内置快照测试配置,涵盖大多数用例。你还可以通过 test-storybook --eject 或在项目根目录中创建 test-runner-jest.config.js 文件来微调配置以满足你的需求。

¥Out of the box, the test-runner provides an inbuilt snapshot testing configuration covering most use cases. You can also fine-tune the configuration to fit your needs via test-storybook --eject or by creating a test-runner-jest.config.js file at the root of your project.

覆盖默认快照目录

¥Override the default snapshot directory

测试运行器默认使用特定的命名约定和路径来生成快照文件。如果你需要自定义快照目录,你可以定义自定义快照解析器来指定存储快照的目录。

¥The test-runner uses a specific naming convention and path for the generated snapshot files by default. If you need to customize the snapshot directory, you can define a custom snapshot resolver to specify the directory where the snapshots are stored.

创建一个 snapshot-resolver.js 文件来实现自定义快照解析器:

¥Create a snapshot-resolver.js file to implement a custom snapshot resolver:

./snapshot-resolver.js
import path from 'path';
 
export default {
  resolveSnapshotPath: (testPath) => {
    const fileName = path.basename(testPath);
    const fileNameWithoutExtension = fileName.replace(/\.[^/.]+$/, '');
    // Defines the file extension for the snapshot file
    const modifiedFileName = `${fileNameWithoutExtension}.snap`;
 
    // Configure Jest to generate snapshot files using the following convention (./src/test/__snapshots__/Button.stories.snap)
    return path.join('./src/test/__snapshots__', modifiedFileName);
  },
  resolveTestPath: (snapshotFilePath, snapshotExtension) =>
    path.basename(snapshotFilePath, snapshotExtension),
  testPathForConsistencyCheck: 'example',
};

更新 test-runner-jest.config.js 文件并启用 snapshotResolver 选项以使用自定义快照解析器:

¥Update the test-runner-jest.config.js file and enable the snapshotResolver option to use the custom snapshot resolver:

./test-runner-jest.config.js
import { getJestConfig } from '@storybook/test-runner';
 
const defaultConfig = getJestConfig();
 
const config = {
  // The default Jest configuration comes from @storybook/test-runner
  ...defaultConfig,
  snapshotResolver: './snapshot-resolver.js',
};
 
export default config;

当测试运行器执行时,它将循环遍历你的所有故事并运行快照测试,为你指定的自定义目录中的项目中的每个故事生成一个快照文件。

¥When the test-runner is executed, it will cycle through all of your stories and run the snapshot tests, generating a snapshot file for each story in your project located in the custom directory you specified.

自定义快照序列化

¥Customize snapshot serialization

默认情况下,测试运行器使用 jest-serializer-html 序列化 HTML 快照。如果你使用特定的 CSS-in-JS 库(如 情感、Angular 的 ng 属性或为 CSS 类生成基于哈希的标识符的类似库),这可能会导致问题。如果你需要自定义快照的序列化,你可以定义自定义快照序列化器来指定快照的序列化方式。

¥By default, the test-runner uses jest-serializer-html to serialize HTML snapshots. This may cause issues if you use specific CSS-in-JS libraries like Emotion, Angular's ng attributes, or similar libraries that generate hash-based identifiers for CSS classes. If you need to customize the serialization of your snapshots, you can define a custom snapshot serializer to specify how the snapshots are serialized.

创建一个 snapshot-serializer.js 文件来实现自定义快照序列化器:

¥Create a snapshot-serializer.js file to implement a custom snapshot serializer:

./snapshot-serializer.js
// The jest-serializer-html package is available as a dependency of the test-runner
const jestSerializerHtml = require('jest-serializer-html');
 
const DYNAMIC_ID_PATTERN = /"react-aria-\d+(\.\d+)?"/g;
 
module.exports = {
  /*
   * The test-runner calls the serialize function when the test reaches the expect(SomeHTMLElement).toMatchSnapshot().
   * It will replace all dynamic IDs with a static ID so that the snapshot is consistent.
   * For instance, from <label id="react-aria970235672-:rl:" for="react-aria970235672-:rk:">Favorite color</label> to <label id="react-mocked_id" for="react-mocked_id">Favorite color</label>
   */
  serialize(val) {
    const withFixedIds = val.replace(DYNAMIC_ID_PATTERN, 'mocked_id');
    return jestSerializerHtml.print(withFixedIds);
  },
  test(val) {
    return jestSerializerHtml.test(val);
  },
};

更新 test-runner-jest.config.js 文件并启用 snapshotSerializers 选项以使用自定义快照解析器:

¥Update the test-runner-jest.config.js file and enable the snapshotSerializers option to use the custom snapshot resolver:

./test-runner-jest.config.js
import { getJestConfig } from '@storybook/test-runner';
 
const defaultConfig = getJestConfig();
 
const config = {
  ...defaultConfig,
  snapshotSerializers: [
    // Sets up the custom serializer to preprocess the HTML before it's passed onto the test-runner
    './snapshot-serializer.js',
    ...defaultConfig.snapshotSerializers,
  ],
};
 
export default config;

当测试运行器执行你的测试时,它将自省生成的 HTML,在快照组件之前将动态生成的属性替换为自定义序列化器文件中的正则表达式提供的静态属性。这可确保快照在不同的测试运行中保持一致。

¥When the test-runner executes your tests, it will introspect the resulting HTML, replacing the dynamically generated attributes with the static ones provided by the regular expression in the custom serializer file before snapshotting the component. This ensures that the snapshots are consistent across different test runs.

使用可移植故事的快照测试

¥Snapshot tests with Portable Stories

Storybook 提供了一个 composeStories 实用程序,可帮助将测试文件中的故事转换为可渲染元素,这些元素可以在使用 JSDOM 的 Node 测试中重复使用。它还允许你将已在项目中启用的其他 Storybook 功能(例如 decoratorsargs)应用到你的测试中,使你能够在你选择的测试环境中重复使用你的故事(例如 JestVitest),确保你的测试始终与你的故事同步,而无需重写它们。这就是我们在 Storybook 中所说的可移植故事。

¥Storybook provides a composeStories utility that helps convert stories from a test file into renderable elements that can be reused in your Node tests with JSDOM. It also allows you to apply other Storybook features that you have enabled your project (e.g., decorators, args) into your tests, enabling you to reuse your stories in your testing environment of choice (e.g., Jest, Vitest), ensuring your tests are always in sync with your stories without having to rewrite them. This is what we refer to as portable stories in Storybook.

你必须 配置你的测试环境使用可移植的故事 来确保你的故事由 Storybook 配置的所有方面组成,例如 decorators

¥You must configure your test environment to use portable stories to ensure your stories are composed with all aspects of your Storybook configuration, such as decorators.

在单个故事上运行测试

¥Run tests on a single story

如果你需要在单个故事上运行测试,则可以使用适当框架中的 composeStories 函数来处理它并应用你在故事中定义的任何配置(例如,decoratorsargs)并将其与你的测试环境结合起来以生成快照文件。例如,如果你正在处理组件并且想要测试其默认状态,确保预期的 DOM 结构不会改变,你可以按以下方式编写测试:

¥If you need to run tests on a single story, you can use the composeStories function from the appropriate framework to process it and apply any configuration you've defined in your stories (e.g., decorators, args) and combine it with your testing environment to generate a snapshot file. For example, if you're working on a component and you want to test its default state, ensuring the expected DOM structure doesn't change, here's how you could write your test:

test/Button.test.js|ts
import { composeStories } from '@storybook/react';
 
import * as stories from '../stories/Button.stories';
 
const { Primary } = composeStories(stories);
test('Button snapshot', async () => {
  await Primary.run();
  expect(document.body.firstChild).toMatchSnapshot();
});

对多个故事执行测试

¥Execute tests on multiple stories

你还可以使用 composeStories 函数测试多个故事。当你想要扩展测试覆盖范围以生成项目中组件不同状态的快照时,这很有用。为此,你可以按如下方式编写测试:

¥You can also use the composeStories function to test multiple stories. This is useful when you want to extend your test coverage to generate snapshots for the different states of the components in your project. To do so, you can write your test as follows:

storybook.test.ts
// Replace your-framework with one of the supported Storybook frameworks (react, vue3)
import type { Meta, StoryFn } from '@storybook/your-framework';
 
import path from 'path';
import * as glob from 'glob';
 
import { describe, test, expect } from '@jest/globals';
 
// Replace your-renderer with the renderer you are using (e.g., react, vue3, svelte, etc.)
import { composeStories } from '@storybook/your-renderer';
 
type StoryFile = {
  default: Meta;
  [name: string]: StoryFn | Meta;
};
 
const compose = (entry: StoryFile): ReturnType<typeof composeStories<StoryFile>> => {
  try {
    return composeStories(entry);
  } catch (e) {
    throw new Error(
      `There was an issue composing stories for the module: ${JSON.stringify(entry)}, ${e}`
    );
  }
};
 
function getAllStoryFiles() {
  // Place the glob you want to match your stories files
  const storyFiles = glob.sync(
    path.join(__dirname, 'stories/**/*.{stories,story}.{js,jsx,mjs,ts,tsx}')
  );
 
  return storyFiles.map((filePath) => {
    const storyFile = require(filePath);
    const storyDir = path.dirname(filePath);
    const componentName = path.basename(filePath).replace(/\.(stories|story)\.[^/.]+$/, '');
 
    return { filePath, storyFile, storyDir, componentName };
  });
}
 
describe('Stories Snapshots', () => {
  getAllStoryFiles().forEach(({ storyFile, componentName }) => {
    const meta = storyFile.default;
    const title = meta.title || componentName;
 
    describe(title, () => {
      const stories = Object.entries(compose(storyFile)).map(([name, story]) => ({ name, story }));
 
      if (stories.length <= 0) {
        throw new Error(
          `No stories found for this module: ${title}. Make sure there is at least one valid story for this module.`
        );
      }
 
      stories.forEach(({ name, story }) => {
        test(name, async () => {
          await story.run();
          // Ensures a consistent snapshot by waiting for the component to render by adding a delay of 1 ms before taking the snapshot.
          await new Promise((resolve) => setTimeout(resolve, 1));
          expect(document.body.firstChild).toMatchSnapshot();
        });
      });
    });
  });
});

当你的测试在测试环境中执行时,它们将生成一个包含项目中的所有故事的快照文件(即 storybook.test.ts|js.snap)。但是,如果需要,你可以使用 Vitest 的 toMatchFileSnapshot API 或 Jest 的 jest-specific-snapshot 包扩展测试文件以为项目中的每个故事生成单独的快照文件。例如:

¥When your tests are executed in your testing environment, they will generate a single snapshot file with all the stories in your project (i.e.,storybook.test.ts|js.snap). However, if you need, you can extend your test file to generate individual snapshot files for each story in your project with Vitest's toMatchFileSnapshot API or Jest's jest-specific-snapshot package. For example:

storybook.test.ts
// Replace your-framework with one of the supported Storybook frameworks (react, vue3)
import type { Meta, StoryFn } from '@storybook/your-framework';
 
import path from "path";
import * as glob from "glob";
 
//👇 Augment expect with jest-specific-snapshot
import "jest-specific-snapshot";
 
import { describe, test, expect } from "@jest/globals";
 
// Replace your-renderer with the renderer you are using (e.g., react, vue3, svelte, etc.)
import { composeStories } from '@storybook/your-renderer';
 
type StoryFile = {
  default: Meta;
  [name: string]: StoryFn | Meta;
};
 
const compose = (
  entry: StoryFile
): ReturnType<typeof composeStories<StoryFile>> => {
  try {
    return composeStories(entry);
  } catch (e) {
    throw new Error(
      `There was an issue composing stories for the module: ${JSON.stringify(entry)}, ${e}`
    );
  }
};
 
function getAllStoryFiles() {
  // Place the glob you want to match your stories files
  const storyFiles = glob.sync(
    path.join(__dirname, 'stories/**/*.{stories,story}.{js,jsx,mjs,ts,tsx}'),
  );
 
  return storyFiles.map((filePath) => {
    const storyFile = require(filePath);
    const storyDir = path.dirname(filePath);
    const componentName = path
      .basename(filePath)
      .replace(/\.(stories|story)\.[^/.]+$/, "");
 
    return { filePath, storyFile, storyDir, componentName };
  });
}
 
describe("Stories Snapshots", () => {
  getAllStoryFiles().forEach(({ storyFile, componentName }) => {
    const meta = storyFile.default;
    const title = meta.title || componentName;
 
    describe(title, () => {
      const stories = Object.entries(compose(storyFile)).map(
        ([name, story]) => ({ name, story })
      );
 
      if (stories.length <= 0) {
        throw new Error(
          `No stories found for this module: ${title}. Make sure there is at least one valid story for this module.`
        );
      }
 
      stories.forEach(({ name, story }) => {
        test(name, async () => {
          await story.run();
          // Ensures a consistent snapshot by waiting for the component to render by adding a delay of 1 ms before taking the snapshot.
          await new Promise((resolve) => setTimeout(resolve, 1));
          // Defines the custom snapshot path location and file name
          const customSnapshotPath = `./__snapshots__/${componentName}.test.ts.snap`;
          expect(document.body.firstChild).toMatchSpecificSnapshot(customSnapshotPath);
      });
    });
  });
});

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

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

Visual Tests 捕获故事图片并将其与图片基线进行比较。快照测试获取 DOM 快照并将其与 DOM 基线进行比较。Visual Tests 更适合验证外观。快照测试对于冒烟测试和确保 DOM 不会改变很有用。

¥Visual tests capture images of stories and compare them against image baselines. Snapshot tests take DOM snapshots and compare them against DOM baselines. Visual tests are better suited for verifying appearance. Snapshot tests are useful for smoke testing and ensuring the DOM doesn’t change.

了解其他 UI 测试

¥Learn about other UI tests