🎄

StorybookをJestで再利用する

2022/12/25に公開

Jestでテストを書く際、Storybookを描画するために設定したデコレーターと同じ設定を
記述しなければいけないのが面倒だなと思っていたところ、@storybook/testing-react
利用するとStoryをJestで再利用出来ると知り早速試してみました。
その際にエラーが所々発生したので解消方法を残しておこうと思います。

Storyを再利用してjestを記述する

まず@storybook/testing-reactをインストールします。

# npm の場合
$ npm install --save-dev @storybook/testing-react

# yarn の場合
$ yarn add -D @storybook/testing-react

そして@storybook/testing-reactのREADMEを参考にJestでストーリーを再利用する様に
テストを修正します。

index.spec.tsx
import React from 'react';
import { render, within } from '@testing-library/react';
import { composeStories } from '@storybook/testing-react';
import '@testing-library/jest-dom';

import * as stories from './index.stories.tsx';

describe('TaskForm', () => {
  it('メッセージが表示されること', async () => {
    const { Success } = composeStories(stories);
    const { container } = render(<Success />);
    const canvas = within(container);
    Success.play({ canvasElement: container });

    expect(await canvas.findByText('タスクが作成されました')).toBeInTheDocument();
  });
});
TaskFormコンポーネント
index.tsx
import React from 'react';

export const TaskForm = () => {
  // 省略

  return (
    <div>
      <h1>タスク作成</h1>
      <form onSubmit={handleSubmit} />
        <div>
          <label htmlFor="title">タスク名</label>
          <input id="title" type="text" placeholder="タスク名を入力してください" />
        </div>
        <button type="submit">作成</button>
      </form>
    </div>
  );
};
TaskFormストーリー
index.stories.tsx
import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';

import { TaskForm } from './TaskForm';

type Story = ComponentStoryObj<typeof TaskForm>;

export default {
  component: TaskForm,
} as ComponentMeta<typeof TaskForm>;

export const Default: Story = {}

export const Success: Story = {
  ...Default,
  play: ({ canvasElement }) => {
    const canvas = within(canvasElement);

    userEvent.type(canvas.getByLabelText('タスク名'), 'title');
    userEvent.click(canvas.getByText('作成'));
  },
};

Storybookではデコレーターを使用しているのでセットアップファイルをjest.config.jsで読み込むように構成を変更します。

setup.jest.js
import { setGlobalConfig } from '@storybook/testing-react';
import * as globalStorybookConfig from './.storybook/preview';

setGlobalConfig(globalStorybookConfig);
preview.js
.storybook/preview.js
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import '../app/assets/stylesheets/application.tailwind.css';

export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};

const client = new QueryClient();
const BaseDecorator = (Story) => (
  <QueryClientProvider client={client}>
    <Story />
  </QueryClientProvider>
);

export const decorators = [BaseDecorator];
jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */

module.exports = {
  roots: ["<rootDir>/app/javascript/src"],
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
+ setUpFiles: ['./setup.jest.js']
};

この状態でテストを実行すると..

Jest encountered an unexpected token

Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.

Details:

/Users/machmap/repo/test/app/frontend/src/test/setup.jest.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import { setGlobalConfig } from '@storybook/testing-react';
                                                                                      ^^^^^^

SyntaxError: Cannot use import statement outside a module

こちらのエラーが発生しました。Node.js では import/export 構文を使用することが
できないので、Jestは setup.jest.js のパースに失敗している様です。

ts-jestのpresetを変更する

https://kulshekhar.github.io/ts-jest/docs/getting-started/presets
ts-jest はESMに変換してくれるpresetsを提供してくれているので、そちらを使用する様に
jest.config.jsを修正します。

jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */

module.exports = {
  roots: ["<rootDir>/app/javascript/src"],
- preset: 'ts-jest',
+ preset: 'ts-jest/presets/js-with-ts-esm',
  testEnvironment: 'jsdom',
  setUpFiles: ['./setup.jest.js']
};

再度テストを実行します。

 Details:

/Users/machamp/repo/test/app/assets/stylesheets/application.tailwind.css:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){@tailwind base;

今度はpreview.jstailwindimportしている箇所でエラーが発生しました。

CSSをモック化

jest-transform-stub を使ってtailwindをモック化することでこのエラーには対応できます。

jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */

module.exports = {
  roots: ["<rootDir>/app/javascript/src"],
  preset: 'ts-jest/presets/js-with-ts-esm',
  testMatch: [
    "**/__tests__/**/*.+(ts|tsx|js)",
    "**/?(*.)+(spec|test).+(ts|tsx|js)"
  ],
  testEnvironment: 'jsdom',
+ transform: {
+   ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub"
+ },
  setUpFiles: ['./setup.jest.js']
};

を追加します。

これで再度テストを実行したところ、テストがパスしました🎉

GitHubで編集を提案

Discussion