Next.js+TypeScript 環境で Storyshots を使う(Storybook 6 対応)

以前の「Next.js+TypeScript 環境で Storybook を使う」で用意した環境に Storybook を使って手軽にスナップショットテストが実行出来る Storyshots を導入します。

Storyshots は Storybook の Story を元にスナップショットテストをテストコードなし(初期設定は必要ですが)で自動的に行なってくれるとても便利なツールです。もし Storybook を整備しているなら是非導入を検討してみていただきたいツールのひとつです。

先に参考にしたドキュメントを掲載します。
この記事を書くために Jest の transpile まわりでかなり時間を使いましたが、以下3つを見ておけばだいたい解決するかと思います。

Snapshot Testing とは

Snapshot Testing は様々なテスト手法のひとつです。Jest の解説記事がわかりやすいので一部引用します。

スナップショットのテストはUI が予期せず変更されていないかを確かめるのに非常に有用なツールです。

モバイルアプリでの典型的なスナップショットのテストケースでは、UIコンポーネントをレンダリングし、スクリーンショットを撮り、テストと一緒に保管しているスナップショットと比較します。 2つのスナップショットが一致しない場合テストは失敗します: 予期されない変更があったか、参照するスナップショットが新しいバージョンのUIコンポーネントに更新される必要があるかのどちらかです。

https://jestjs.io/docs/ja/26.2/snapshot-testing

上記のとおり現在の状態を正しいと定義し、その出力結果をスクリーンショットや json などの状態でスナップショットとして保存しておき、コード変更後に新たな出力結果とスナップショットを比較します。そうすることで意図しない変更が発生していないかを検知する仕組みです。

また、 Jest ではスクリーンショットではなく React などのコンポーネントのツリー構造をコードとして保存するのでテスト結果も diff で表示され変更箇所を把握しやすくなっているのも特徴です。

Storybook のメジャーバージョンをあげる

ということで早速導入したいところですが、Storybook 6 が2020年8月11日にリリースされたので5から6へあげます。
Storybook 6 では Zero-config Storybook として Built-in TypeScript support になりました 🎉
これで以前の記事の1/3が要らない子になりましたね。後で追記しておきます。。

Storybook のメジャーバージョンをあげる作業のPRは下記です。

メジャーバージョンを 6 にあげる

まずは Storybook 関連のパッケージを upgrade します。

$ yarn upgrade @storybook/addon-links @storybook/react

不要になった ts 関連の記述を削除

Storybook が ts 対応したので不要になったパッケージと設定ファイルを削除します。

$ yarn remove babel-loader
$ rm .storybook/main.js // ここで削除した main.js は次で再度作成します

※PRでは babel-preset-react-app も削除していますが、後続の作業で必要でした。

config の指定方法が変わったので対応

正確には v6 ではなく v5.3 3 での変更ですが、.storybook/addons.js と .storybook/config.js が deprecated になりました。
どちらも v7 では非対応になるとのことでこのタイミングで修正しておきます。
詳しくは: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#to-mainjs-configuration

story の場所や使用する addon 設定は main.js にまとめることになりました。

module.exports = {
  stories: ["../components/**/*.stor@(y|ies).[tj]sx"],
  addons: ["@storybook/addon-links/register"],
};

Welcome という story をナビゲーションの一番上に移動する処理 storySort を config.js に書いていましたが、ブラウザで実行する処理は preview.js に移動になりました。
(ついでにソートの仕方も目的にあわせてシンプルにしました。)

const addParameters = require("@storybook/react").addParameters;

addParameters({
  options: {
    storySort: {
      order: ["Welcome", "README"],
    },
  },
});

Jest の導入

Storyshots は Jest を使っているのでまずは Jest を導入します。
このプロジェクトの Next.js はゼロから create-next-app しているので Jest が入っていません。Next.js 公式の下記を参考に Jest の導入をしました。

Jest の導入に関するPRは下記の前半です。

まずは使用するパッケージをインストールします。

$ yarn add --dev \
           jest \
           jest-watch-typeahead \
           react-test-renderer \
           babel-jest \
           babel-preset-react-app \
           @types/jest

そして Next.js 公式の examples/with-typescript-eslint-jest を参考に設定ファイルを作成します。

とくに .babelrc で presets に next/babel を指定しているところが最初は分からずドハマりしていました。Next.js 関連は最初に公式のサンプルから確認するのがおすすめです。

{
  "presets": ["next/babel"]
}

動作確認用に true が toBeTruthy になるかという雑なテストを追加しました。

import "@jest/globals";

describe("jest 動作確認", () => {
  test("true toBeTruthy", () => {
    expect(true).toBeTruthy();
  });
});

これで package.json の test を追加して yarn test すれば jest の動作確認ができました。

  "scripts": {
  ...
    "test": "jest"
  },
yarn test 実行時のターミナルのスクリーンショット
サンプルのテストコードが pass している様子

Storyshots の導入

Jest が導入できて Storybook が正しく設定できていれば Storyshots の導入自体は簡単です。パッケージをインストールし、Storyshots 用のテストコードを一つ用意すれば導入は完了です。
今回は通常の test と Storyshots を分けて実行したかったので少し手順が多くなっています。

Storyshots の導入に関するPRは下記の後半です。

Storyshots の基本的な導入

@storybook/addon-storyshots をインストールし

$ yarn add --dev @storybook/addon-storyshots

Storyshots.test.ts を作成します。

import initStoryshots from "@storybook/addon-storyshots";

initStoryshots();

これで $ jest を実行すれば jest の他のテストと一緒に Storyshots も実行されます。

Storyshots のみ実行するように script を分ける

$ yarn storyshots で Storyshots のみ実行できるように package.json に script を追加し、専用の jest config を用意します。

  "scripts": {
  ...
    "storyshots": "jest --config ./jest.storyshots.config.js",
    "test": "jest"
  },
const baseConfig = require("./jest.config");

module.exports = {
  ...baseConfig,
  name: "Storyshots",
  displayName: "storyshots",
  testMatch: ["<rootDir>/Storyshots.test.ts"],
};

Snapshots テストを実行してみる

初回実行時

すべての準備がととのったので Storyshots を実行します。
初回実行時は自動的にスナップショットが生成されてテストがパスします。

$ yarn storyshots
yarn storyshots 実行時のターミナルのスクリーンショット
スナップショットテストが実行された様子

 ※複数の Story で同じ `Welcome` タイトルを使っているという警告がでていますが、今回とは関係ないので無視します。

これで下記のスナップショットファイルが生成されます。

  • __snapshots__/Storyshots.test.ts.snap

Story を編集したあとのテスト実行

その後 Button.withText Story を編集してもう一度実行すると下記のように差分を検知しテストが fail します。

Story を編集してから yarn storyshots を再実行した時のターミナルのスクリーンショット
スナップショットテストが fail した様子

Hello Button が Hello Button! に変わっているのがわかります。

スナップショットの更新

もしこの変更が正しい場合はスナップショットの更新 jest --updateSnapshot を行います。
yarn storyshots の本体は jest なので --updateSnapshot オプションをつけて実行するだけです。

$ yarn storyshots --updateSnapshot

これでさきほど生成された __snapshots__/Storyshots.test.ts.snap が更新されて、もう一度 $ yarn storyshots を実行しても fail しなくなります 🎉

Storybook を資産として有効活用していきたい

Gaji-Labo ではコンポーネントを Storybook 上で実装してからアプリケーションに反映していくフローを採用しています。
ただ、長く運用しているプロジェクトでは Storybook のメンテナンスが行き届かなくなっていくことも多々あります。その理由のなかに「Storybook をメンテするメリットが薄い」ためにモチベーションが維持できないというのもあると感じています。
Storyshots の導入で少しでも Storybook をメンテナンスする動機づけになればいいなとおもっています。Gaji-Labo の手から離れてもコンポーネントの秩序が保たれてプロダクトの寿命を少しでも伸ばし日々の開発効率を下支えできるような状態を作り続けたいです。

開発のお悩み、フロントエンドから解決しませんか?

あなたのチームのお悩みはなんですか? 「腕の良いエンジニアに重要でない作業まで任せてしまっている」「腕の良いデザイナーに主業務以外も任せてしまっている」「すべての手が足りず細かいことまで手が回らない」などなど… 。

そんなときは、相談相手としてGaji-Laboにお気軽にお声がけください。あなたの開発チームに足りていない役割や領域を適切に捉えてカバーすることで、チーム全体の生産性と品質をアップさせるお手伝いをします。

オンラインでのヒアリングとフルリモートでのプロセス支援に対応していますので、リモートワーク対応可のパートナーをお探しの場合もぜひ弊社にお問い合わせください!

お悩み相談はこちらから!


投稿者 原田 直貴

受託と事業会社の両方を経験し、沢山の事業を見てみたい気持ちで Gaji-Labo を共同創業。普段は雑用やったりプロジェクトマネジメントやったり、たまにフロントエンドのコードを書いたり。直近は Gaji-Labo をデザイン会社に転換していく課題に挑戦中。期待値コントロールにステ全振り。