コンポーネント/単体テスト単位でのvisual regressionテストを行うためのツールを作った話し

この記事は[「WACUL Advent Calendar 2016」](http://qiita.com/advent-calendar/2016/wacul)の23日目です。
今年の10月からWACULでフロントエンドエンジニアをしている@bokuwebと申します。

今回はコンポーネント単位での`visual regressionテスト`について書いてみたいと思います。

## 概要

単体テスト時にコンポーネントのキャプチャーを取り、差分を取る`visual regression`テストを行うために、`karma-nightmare`と`reg-cli`というツールを作りました。まだ実験的ではありますが、それらを用いたテストや作成時に検討したことなどを書いてみます。

## そもそもViewのテストどうしてます？

フロントエンドやっている方と話すときに、よく、`View`のテストをどこまで、どんなふうにやるか？というざっくりした質問をしてみたりします。「お、その話題ですか！」というリアクションする方もちらほらいて関心度は高そうなんですが、「これが決定版！」みたいな手法は聞いたことはなく、みんな工夫して頑張っているんだな、という印象をちょくちょく受けます。

`React`などを使用しており多くのコンポーネントが冪等性を担保したコンポーネントであれば、`smart component`や`store`などの`state`生成部をしっかりテストし、`View`のテストは上手くサボるのもありだと思いますが、やはり、入力に対して複雑な分岐などが存在するコンポーネントなどはやはり単体テストでしっかり抑えておきたいところです。

現在`React`であれば、`Enzyme`などを使用し、`props`を与え「`<Hoge />`が`レンダリングされていること`」のように、`assert`していくのが一般的でしょうか。
しかし`View`は変更が発生しやすい箇所ですし、やるにしても緩く最小限の手間で効果を上げたいという思いはみんな持っているんじゃないでしょうか。

すこし横道にそれますが最近`Jest`に搭載された`snapshot`テストはそういう意味では自分のやりたい方法に近いと感じました。

## `Jest`の`snapshot`テスト

たとえば以下のような(`react-create-app`で生成した)コンポーネントに対して、`snapshot`テスト行ってみます。

`snapshot test`は、一度テストを実行するとスナップショットのファイルが作成されて、次回以降はスナップショットファイルとテストが一致するかどうかを判定され、一致していればテストにパスします。

- comonent 

``` Javascript:my-app/src/App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}
export default App;
```

- test

``` Javascript:my-app/src/App.test.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import renderer from 'react-test-renderer';

it('renders without crashing', () => {
  const rendered = renderer.create(
    <App />
  );
  expect(rendered.toJSON()).toMatchSnapshot();
});
```

テストを実行すると以下のような`snapshot`ファイルが保存され、次回以降このファイルとの比較が行われ、差分があった場合にテストが失敗するようになります。


```html:my-app/src/__snapshots__/App.test.js.snap
exports[`test renders without crashing 1`] = `
<div
  className="App">
  <div
    className="App-header">
    <img
      alt="logo"
      className="App-logo"
      src="logo.svg" />
    <h2>
      Welcome to React
    </h2>
  </div>
  <p
    className="App-intro">
    To get started, edit 
    <code>
      src/App.js
    </code>
     and save to reload.
  </p>
</div>
`;
```

その後、少しテキストを変更し再度テストを行うと、以下のように差分を表示し、テストが失敗しました。

差分を確認後問題なければ、`u`オプションを付けてテストを再実行することにより`snapshot`ファイルが更新され、次回以降テストの基準となります。


![スクリーンショット 2016-12-20 8.45.21.png](https://qiita-image-store.s3.amazonaws.com/0/91510/ac0feb2f-87d8-9262-f5d1-79c709bacf5d.png)


## スクリーンショットによる差分テスト

`Jest`の`snapshot`テストは多少目視による確認は必要としますが、ざっくりと`View`のテストが行えるため、`View`のテストに対するコストは下がりそうです。

入力をざくっと与えて正となる出力を作っておけばOKですからちまちま`Assert`していく必要は減りますし、変更があった場合のテストの修正も楽です。

ただ、この方法の場合、実際のブラウザ上での見た目がどうなっているかは分からないという問題があります。極端な例になりますが、誰かが、`.App-intro { display: none }`のような`css`を書いて、テキストが非表示になっていてもこのテストは`pass`し続けてしまいます。また、`class`名をtypoしていて実はスタイルが当たってない、なんてこともあるかもしれません。

この辺の**見た目**に関しては`E2E`テストでキャプチャを取って確認するケースもあるかとは思いますが、`E2E`のようにページ単位になるとどのコンポーネントが問題なのかわかりづらいですし、複雑な分岐をもったコンポーネントなどが合った場合、その条件を`E2E`で網羅するのは現実的ではないし`E2E`の責務ではありません。

前置きが長くなりましたが、上記のような経緯や考え方からコンポーネント単位で開発している場合は、やはり、**コンポーネント単位で見た目も含めてテストしていきたい**、**描画に差分が発生した場合はテストに失敗して欲しい**という考えから、スクリーンショットによる差分テストが行えないかと考えました。(そもそも業務では`Angular v2`を使っているので`Jest`が使えないですし・・。)

つまりは先に挙げた`Jest`の`snapshot`テストの画像版のイメージです。[^0]

### 単体テスト時のスクリーンショットを取る

まずは単体テストのスクリーンショットを取るための`karma-nightmare`というものを作りました。方法はいくつか考えたんですが、ひとまずこの方法に落ち着いています。[^1]

- https://github.com/bokuweb/karma-nightmare

`karma-nightmare`は`karma`から`nightmare`を立ち上げて`nightmare`上でテストを実行するランチャーです。`nightmare`は`electron`のラッパーのなので、これで`Chromium`上でテストを実行しつつ、`capturePage` APIを使用することでそのスクリーンショットも取ることができます。

もう少し手を加えたら`pdf`や`html`で保存することも可能です。

また副産物なんですが、`karma`でのテストで`require`やその他`node`モジュールが使えるという利点もあります。

ここでは`React`を使って簡単なサンプルを書いてみます。
以下のリポジトリも合わせて参考にしてみてください。

- https://github.com/bokuweb/react-reg-test-sample

``` Javascript:src/index.js
import React, { Component } from 'react';

export default class HelloWorld extends Component {
  render() {
    return (
      <h1 style={{ color: '#fff', fontFamily: 'arial' }} >hello</h1>
    );
  }
}
```

上記のようなコンポーネントに対して、以下のテストを実施してみます。`enzyme`で`mount`し、`karma-nightmare`の`screenshot`を使用してスクリーンショットを取っています。

ちみにこの`screenshot`ですが、`nightmare`上で走っているときのみ実行されるので、他のブラウザ`chrome`や`phantomjs`では無視されるだけで、影響を与えることはありません。なのでこのテストコードはこのまま他のブラウザでも実行することができます。

ここではベタに画像ファイル名を指定していますが`mocha`では`this`経由でspec名が取れるのでspec名 = 画像名などにしてやると良いかもしれません。(ここには挙げませんが`Jasmine`では残念ながらテスト名を取るには一工夫いるようです。)

``` Javascript:test/helloworld.spec.js
import React from 'react';
import { screenshot } from 'karma-nightmare';
import { mount } from 'enzyme';
import HelloWorld from '../src/index';

describe('Hello', () => {
  it('Should render hello', (done) => {
    mount(
      <HelloWorld />,
      { attachTo: document.querySelector('.main') },
    );
    screenshot('./docs/snapshot/hello.png').then(done);
  });
});
```

実行すると以下のような`screenshot`が取得できます。(背景は予め`css`で、マウントしている要素にあてられています。) [^2]

![hello.png](https://qiita-image-store.s3.amazonaws.com/0/91510/de988957-09bd-79a5-ce85-c633d03107da.png)

これでスクリーンショットを取る準備はできました。

### スクリーンショットの差分比較を行う

次にスクリーンショットの差分比較を行うため、`reg-cli`というツールを作りました。

- https://github.com/bokuweb/reg-cli

こいつは非常にシンプルなもので、取得した画像ディレクトリと、見本となる画像ディレクトリを指定してやると差分をとってhtmlレポートを吐くような作りになっており、`Jest`を踏襲して`-U`オプションを付けて実行すると見本となるディレクトリに画像がコピーされ、見本が更新されるという作りになっています。

そのため、`React`や`Angular`などのコンポーネントのテストのみならず、`E2E`で取ったスクリーンショットの比較や、生成画像比較したいケースなどにも使用できます。

実際に実行してみます。

##### 1. 生成された画像に対してコマンドを実行してみる

先と同様、以下のリポジトリも合わせて参考にしてみてください。

- https://github.com/bokuweb/react-reg-test-sample
 
今回は`docs/snapshot/`にスクリーンショットを吐くようなテストになっているので以下のコマンドを実行します。これで`./docs/expected`を見本ディレクトリに、`./docs/diff`を差分画像保存ディレクトリに、`./docs/report.html`にレポートを吐くように動作します。

``` sh
$ reg-cli ./docs/snapshot ./docs/expected ./docs/diff -R ./docs/report.html
```

以下のように新しい画像が検出されたメッセージとレポートが生成され、テストが失敗します。

``` sh
❋ 1 new images detected.
  ✚ ./docs/snapshot/hello.png

Inspect your code changes, re-run with `-U` to update them.
```

`./docs/report.html`

![スクリーンショット 2016-12-23 0.46.42.png](https://qiita-image-store.s3.amazonaws.com/0/91510/0726fddc-5b4f-561b-5011-b5b0994020f9.png)

##### 2. 画像に問題なければ見本となる画像更新する

画像に問題がなければ`-U`をつけて再度実行します。これにより見本となる画像が更新され、テストに通るようになります。(この辺の手間をもう少し減らすように考えたいですね)


``` sh
$ reg-cli ./docs/snapshot ./docs/expected ./docs/diff -R ./docs/report.html -U
```

`./docs/report.html`

![スクリーンショット 2016-12-23 0.47.07.png](https://qiita-image-store.s3.amazonaws.com/0/91510/5fa083be-257c-cdf3-ebf8-01107f94e3c8.png)

##### 3. コンポーネントの内容を変更し再度テストしてみる

コンポーネントに何かしらの変更が発生したと仮定し、以下のようなテキストを変えてみます。

``` Javascript:src/index.js
import React, { Component } from 'react';

export default class HelloWorld extends Component {
  render() {
    return (
      <h1 style={{ color: '#fff', fontFamily: 'arial' }} >awesome</h1>
    );
  }
}
```

テストを実行します。

``` sh
$ npm t && reg-cli ./docs/snapshot ./docs/expected ./docs/diff -R ./docs/report.html
```

意図通りテストが失敗しました。

``` sh
✘ 1 test failed.
  ✘ ./docs/snapshot/hello.png

Inspect your code changes, re-run with `-U` to update them.
```

以下のようなレポートが吐かれています。ちょっと差分画像にはまだ工夫がいりそうですがコンポーネントに変更が入った際、テストが落ちるため、`あるコンポーネントAを変更したら、知らず知らずの内の他のAに依存したコンポーネントが崩れていた。`のような状況を避けることができます。

https://bokuweb.github.io/react-reg-test-sample/report.html

![スクリーンショット 2016-12-23 9.06.00.png](https://qiita-image-store.s3.amazonaws.com/0/91510/df0d6568-8d07-1d3d-fa75-f308cbe9bc99.png)


## 課題

ここまでで一応はコンポーネント単位でスクリーンショットをとり差分の比較ができるようになりましたが、まだ次のような課題があります。

- 画像を生成するマシンによって微妙に生成される画像が異なるケースが存在する[^3]

画像差分比較側でしきい値を設けある値以下の差であれば同一画像とみなす。ということも可能ですがこれは抜本的な対策にはならないため(機能としては追加しても良さそうですが)、やはり、CIなどで画像を生成するマシンを固定し、管理する必要がありそうです。

そのための仕組みは今ないので、追って検討する必要がありそうです。

また、レポートの見た目や機能がいまいちなので随時更新していく予定です。

## Angular v2でのサンプル

@Quramy が早速Angularでのサンプルを作成してくれたので紹介いたします。この記事のサンプルより、いい感じのサンプルなので是非合わせて見てみてください。

https://github.com/Quramy/karma-nightmare-angular-demo

## まとめ

`View`のテストでいかにサボりつつ効果をあげるかという考えをもとにツールを作り、使用してみました。まだまだ課題はありますが、ブラシアップしより良いものになった際は再度記事にしてみたいと思います。

この内容がテストの参考になれば幸いです。

最後まで読んでいただきありがとうございました。

### 脚注

[^0]: プルダウンのアイテムが動的に生成されているようなケースは画像より、`生成されたDOM`を見たほうがよく、ケースにより一長一短ではあります。

[^1]: `PhantomJS`であればキャプチャー機能を持っているので、最初は`PhantomJS`から試していたんですが、`flexbox`に対応していなかったり、見た目にいろいろ難があったのでやめました。また、`karma`の`middleware`を使って、`endpoint`を叩かせ、`node`コンテキスト側でキャプチャを取ろうとも思いましたが、これも面倒そうでやめました。

[^2]: 余談ですが、`Karma v1`から`contextFile`という`html`ファイルを設定することができるようになりました。これにより、工夫すればプロダクションの環境に近づけて単体テストを行うことができます。今回は次のような`context.html`を使用しています。https://github.com/bokuweb/react-reg-test-sample/blob/master/test/context.html

[^3]: 同じmacにもかかわらず、同僚の作った見本画像に対して、自分のmacで作った画像を差分比較すると数ドットながら差分が発生する、ということがありました（原因は未追跡）。