Storybook + StoryShotsでReactをスタイルガイド運用、CIしていく知見


2016/10/07追記:StorybookとStoryShotsについて説明してくれている記事が出てきたため、こちらの記事はもう少し深入りした部分にフォーカスするように(install関連のとこなどを主にばっさり削除)しました

Storybookとは

Reactのためのスタイルガイドジェネレータ。

基本的な所は結構先人が記事を残してくれている。
特に今回この記事ではstoriesそのものの記載については言及しないので、そこらへんは下記の記事などを参考にするのが良いだろう

だいぶ手に馴染んできてたので、自分がよく使う部分中心に、基本的によく使う設定 + CIで運用する事あたりまでについて記載してきたい。

Storybook

よく使う or 基本的な設定、カスタマイズ

config.js

.storybook/config.jsに配置される。Storybookの基本設定ファイル。

特定のパターンで動的読み込み

だいたいstoriesファイルはまとめて一箇所に置くより、各コンポーネントの近い所に配置したい。

そうなると動的読み込みが活躍する。

だいたいこんな具合

storybook/config.js
// `../src/`がターゲットディレクトリ。
// `hoge.stories.js`みたいなファイルを対象とする。
const req = require.context('../src/', true, /.stories.js$/)

function loadStories() {
  req.keys().forEach((filename) => req(filename))
}

configure(loadStories, module)

Decorator

Storybook上の表示の共通化のために、Decoratorという仕組みが用意されている。

「全部のコンポーネント外枠つけたいな。」みたいな場合がよくあるので、こんな感じで使う。

storybook/config.js

const LayoutDecorator = (story) => (
  <MyLayout>
    {story()}
  </MyLayout>
)

function loadStories() {
  addDecorator(LayoutDecorator)
  req.keys().forEach((filename) => req(filename))
}

configure(loadStories, module);

特定のstorydだけで使うことも可能。

babelの設定

.babelrcがあれば読み込んでくれる。
特にいじったことは今のところ無いが、個別に設定したい場合は.storybook/.babelrcに配置する。

Webpack設定

Storybookはwebpackを裏で動かしている。
設定ファイルは.storybook/webpack.config.jsとして配置する。
webpackをプロジェクトのビルドをしてるなら特に考える事も無いかもしれない。

自分の環境では良くビルドにはbrowserifyrollupを利用する事もあるが、storybookの部分はproductionビルドには関わって来ないので特に問題にならない。
(正確には、browserify側とStorybook側の設定値で同じような設定を二箇所でやらなければならないでちょっと冗長になるが、ギリ耐えられるという感じ。)

自分の場合、パス解決だけよく必要になったりするので、こんな感じで設定したりしたりする。

const path = require('path')
module.exports ={
  resolve : {
    root: path.resolve(__dirname, '../src')
  }
}

ヘッダファイルはhead.htmlで加工

CSSを外部管理していたり、normalize.cssやsanitize.cssなどのreset系CSSを外から読みたかったり、フォントを読み込みたかったり する場合など、<head>タグに記載したい諸々は.storybook/head.htmlで管理する
参考:Add Custom Head Tags

sanitize.cssをCDNから読むからこんな具合

head.html
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/4.1.0/sanitize.css">

StoryShotsでStorybookにCIをもたらす

スタイルガイドはよく腐る。
しかし単なる「スタイルガイド」という視点ではなく「描画確認可能なのUnit Testだ」と考えたらどうだろう?少しは希望を感じる。

そんな希望に応えそうなのがStoryShots

Storybookで記載したコンポーネントをsnapshotとして保存するテストを行うことが出来る。これはJestなどでも行われている手法。
(参考:Snapshot Testing in React Storybook

Jestでスナップショットテストをするのは良いと思うが、表示確認が出来るという部分で個人的にはStoryShotsに分があると思っている。

また、やってみた感想にはなるが、「Unit Testを書いているんだ」という気持ちでstoriesファイルを書いてみると、コンポーネントを疎結合に書くことを意識できる良い効能もあった。

ハマり回避テクニック

loader, polyfillでハマり回避

ブラウザで動かすように作られたコードをCIするのはやっぱりまだまだ辛い。
StoryShotsではこの問題に対し、loaderpolyfillsというオプションが用意されている。

loadercssjpgなどwebpackのloaderを利用しているようなところをなんとかするものっぽい。自分はまだ世話になったことがない。

polyfillsはjs側のコード的なところを調整するもの。
src/default_config/polyfills.jsからコピーして使う。

clipboard.jsを利用したコンポーネントで自分がハマったときは下記二行を足した。

global.Element = global.window.Element;
global.HTMLElement = global.window.HTMLElement;

テストスクリプトもこんな感じで書き換える。 

{
    :
    "test-storybook": "storyshots --polyfills=.storybook/polyfills.js"
    :
}

テスト時だけmock化したい部分はNODE_ENV=testで回避

当たり前だが、乱数などを使ってる箇所はsnapshotテストではコケる。
こういう箇所に対しては、NODE_ENV=testで部分的なmockにすると良いだろう。
hack的ではあるが、まれによく見る慣例だ。

例えばuuidを使うような箇所はこんな感じにする。

import uuid from 'uuid'

export default () => {
  if(process.env.NODE_ENV === 'test'){
    return 'unique-id'
  }
  return uuid()
}

テストの呼び出しにもNODE_ENVを設定する

{
    :
    "test-storybook": "NODE_ENV=test storyshots"
    :
}

ちょっと冗長にはなるが、保守性は高くなるし費用対効果としては悪くないと思っている。

テスト出来ないコンポーネントは諦める

身も蓋もない!という部分もあるが、とはいえCIにあんまり頑張りすぎて疲弊してもしょうがない。
無理を感じたら、一部のstoriesについてはいっそ諦めるのも一つの手だろう。

-gでgrepして一部絞込、-xで一部除外などがある。

npm run test-storybook -- -g "grep_target"
npm run test-storybook -- -x "exclue"

Decoratorを利用してskipする

DecoratorNODE_ENV=testを利用して、テスト時にスキップするデコレータを明示的に記載することも考えられる

const TestSkipDecorator = (story) => {
  if(process.env.NODE_ENV === 'test'){
    return <div>Test Skip</div>
  }
  return story()
}

storiesOf('story', module)
  .addDecorator(TestSkipDecorator)
  .add('Module', () => (
    <SomeModule />
     :

CIを回す

通常のテストとは分離して定義して、npm-run-allなどを使ってCI上での実行をする

package.json
"scripts": {
    :
  "test": "mocha",
  "test-storybook": "storyshots",
  "test:all": "npm-run-all test test-storybook"
}
circle.yml
test:
  - npm run test:all

番外:もっと色々テストしたいなら?

個人的には保守性のトレードオフを考えると、あまり複雑にテストするより、snapshotを保存するだけのStructural Testで十分に思えるが、下記の手法も紹介されている。