文件
Storybook Docs

快照測試

快照測試會將每個 Story 渲染後的標記與已知的基準進行比較。這是一種識別觸發渲染錯誤和警告的標記變更的方法。

Storybook 是快照測試的實用工具,因為每個 Story 本質上都是一個測試規範。每當您編寫或更新 Story 時,您都會免費獲得一個快照測試。

Example Snapshot test

如果您正在升級到 Storybook 8.0 並且之前使用 Storyshots 附加元件進行快照測試,則該附加元件已在此版本中正式棄用並移除。請參閱遷移指南以獲取更多資訊。

使用 test-runner 自動化快照測試

Storybook test-runner 將您的所有 Story 轉變為可執行的測試。由 JestPlaywright 驅動。它是一個獨立、與框架無關的工具,與您的 Storybook 並行運行。它使您能夠在多瀏覽器環境中運行多種測試模式,包括使用 play 函式的元件測試、DOM 快照和無障礙測試

設定

若要使用 test-runner 啟用快照測試,您需要採取額外步驟來正確設定它。我們建議您在繼續進行其餘必要設定之前,先閱讀test-runner 文件,以了解更多關於可用選項和 API 的資訊。

在您的 Storybook 目錄中新增一個設定檔,內容如下

.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 Hook 允許您擴展測試執行器的預設設定。請在此處閱讀更多相關資訊。

當您執行測試執行器 (例如,使用 yarn test-storybook) 時,它將遍歷您的所有 Story 並運行快照測試,為您專案中的每個 Story 生成一個快照檔案,該檔案位於 __snapshots__ 目錄中。

設定

測試執行器開箱即用,提供了涵蓋大多數使用案例的內建快照測試設定。您也可以透過 test-storybook --eject 或在專案根目錄建立 test-runner-jest.config.js 檔案來微調設定以符合您的需求。

覆寫預設快照目錄

預設情況下,測試執行器對生成的快照檔案使用特定的命名慣例和路徑。如果您需要自訂快照目錄,您可以定義自訂快照解析器來指定快照的儲存目錄。

建立一個 snapshot-resolver.js 檔案來實作自訂快照解析器

./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 選項以使用自訂快照解析器

./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;

當測試執行器執行時,它將循環遍歷您的所有 Story 並運行快照測試,為您專案中的每個 Story 生成一個快照檔案,該檔案位於您指定的自訂目錄中。

自訂快照序列化

預設情況下,測試執行器使用 jest-serializer-html 來序列化 HTML 快照。如果您使用特定的 CSS-in-JS 函式庫 (例如 Emotion)、Angular 的 ng 屬性或類似的函式庫 (這些函式庫會為 CSS 類別產生基於雜湊的識別碼),則可能會導致問題。如果您需要自訂快照的序列化,您可以定義自訂快照序列化器來指定如何序列化快照。

建立一個 snapshot-serializer.js 檔案來實作自訂快照序列化器

./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 選項以使用自訂快照解析器

./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,在對元件進行快照之前,將動態產生的屬性替換為自訂序列化器檔案中正規表示式提供的靜態屬性。這確保了快照在不同的測試運行中保持一致。

使用可攜式 Stories 進行快照測試

Storybook 提供了一個 composeStories 工具,可協助將測試檔案中的 Stories 轉換為可渲染的元素,這些元素可以在您的 Node 測試中與 JSDOM 重複使用。它還允許您將您在專案中啟用的其他 Storybook 功能 (例如,decoratorsargs) 應用到您的測試中,使您能夠在您選擇的測試環境 (例如,JestVitest) 中重複使用您的 Stories,確保您的測試始終與您的 Stories 同步,而無需重寫它們。這就是我們在 Storybook 中所說的可攜式 Stories。

必須設定您的測試環境以使用可攜式 Stories,以確保您的 Stories 與您的 Storybook 設定的所有方面 (例如 decorators) 組合在一起。

在單個 Story 上運行測試

如果您需要在單個 Story 上運行測試,您可以使用適當框架的 composeStories 函式來處理它,並應用您在 Stories 中定義的任何設定 (例如,decoratorsargs),並將其與您的測試環境結合以生成快照檔案。例如,如果您正在開發一個元件,並且想要測試其預設狀態,確保預期的 DOM 結構沒有變更,以下是您可以編寫測試的方式

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

在多個 Stories 上執行測試

您也可以使用 composeStories 函式來測試多個 Stories。當您想要擴展測試覆蓋率以為專案中元件的不同狀態生成快照時,這非常有用。為此,您可以如下編寫測試

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

當您的測試在您的測試環境中執行時,它們將生成一個包含您專案中所有 Stories 的單個快照檔案 (即storybook.test.ts|js.snap)。但是,如果需要,您可以擴展您的測試檔案,以使用 Vitest 的 toMatchFileSnapshot API 或 Jest 的 jest-specific-snapshot 套件為您專案中的每個 Story 生成單獨的快照檔案。例如

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

快照測試和視覺測試之間有什麼區別?

視覺測試捕獲 Stories 的圖像並將其與圖像基準進行比較。快照測試獲取 DOM 快照並將其與 DOM 基準進行比較。視覺測試更適合驗證外觀。快照測試對於冒煙測試和確保 DOM 不會變更很有用。

了解其他 UI 測試