30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

JestでReactのテストをする(3) 仮想DOMのテスト

前回はクリックイベントのテストを行った。
今回は、DOMが正しくレンダリングされているかのテストを行う。
正確には、DOMではなくて、仮想DOMであり、コンポーネントと言ったほうがいいのだと思う。

テストの対象となるコード

まずは、テストの対象となるコードをdom.jsとして書く。

// dom.js
import React from 'react';

const Profile = props=>{
  const {data} = props;
  return(
    <div>
      名前:{data.name}<br />
      年齢:{data.age}
    </div>
  );
};

export { Profile };
// dom-render.js
import React from 'react';
import ReactDOM from 'react-dom';
import {Profile} from './dom.js';

const data = {name: 'Tom', age: 30};
ReactDOM.render(<Profile data={data} />, document.getElementById('mount'));

これで、dom-render.jsをビルドしてhtmlで読み込むと、ブラウザには次のように表示される。

名前:Tom
年齢:30

このコードをテストしていく。

テストを書く

準備として、enzyme-to-jsonをインストールする。

npm install -S enzyme-to-json

テストコードは以下の内容。

// dom.test.js
import React from 'react';
import { shallow } from 'enzyme';
import { shallowToJson } from 'enzyme-to-json';
import {Profile} from '../dom.js';

describe('domのテスト', ()=>{
  test('Profileコンポーネントをレンダリングする', ()=>{
    const elem = shallowToJson(shallow(<Profile data={{name: 'Tom', age: 30}} />));
    expect(elem).toMatchSnapshot();
  });
});

実行結果。

 PASS  __tests__/dom.test.js
  domのテスト
    ✓ Profileコンポーネントをレンダリングする (11ms)

Snapshot Summary
 › 1 snapshot written in 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 added, 1 total
Time:        5.621s
Ran all test suites matching "__tests__/dom.test.js".

後述するが、toMatchSnapshot()のテストは、シンタックスエラーなどが無い限り、一度目は必ずパスする仕組みになっている。

shallowToJson()

対象となるコンポーネントshallowレンダリングし、その戻り値をshallowToJsonの引数として渡すことで、そのレンダリングの内容をテストできるようになる。

なお、shallowshallowToJsonではなく、mountmountToJsonでも、同じことが出来る。

toMatchSnapshot()

expect(shallowToJsonの戻り値).toMatchSnapshot();

このようにすることで、shallowレンダリングした内容をテストできる。

スナップショットの仕組み

toMatchSnapshot()を実行すると、テストファイルの同階層に__snapshots__というフォルダが作られ、そのなかに<テスト実行ファイル名>.snapが作られる。今回の例だと、dom.test.js.snap
以下が、その中身。

// dom.test.js.snap

exports[`domのテスト Profileコンポーネントをレンダリングする 1`] = `
<div>
  名前:
  Tom
  <br />
  年齢:
  30
</div>
`;

初回テスト時や、対応するスナップショットがない場合は、このようにスナップショットが作成される。そしてその際、テストは必ずパスする。
2回目以降のテスト、つまり既にスナップショットが存在する場合は、今回のレンダリングの内容と、保存してあるスナップショットとを比較して、テストする。

スナップショットの中身を変更してみる。

exports[`domのテスト Profileコンポーネントをレンダリングする 1`] = `
<div>
  名前:
  Bob
  <br />
  年齢:
  45
</div>
`;

こうすると、意図通り、テストはパスしなくなる。
そこで、パスするよう、テストを書き換えていく。

import React from 'react';
import { shallow } from 'enzyme';
import { shallowToJson } from 'enzyme-to-json';
import {Profile} from '../dom.js';

describe('domのテスト', ()=>{
  test('Profileコンポーネントをレンダリングする', ()=>{
    const elem = shallowToJson(shallow(<Profile data={{name: 'Bob', age: 45}} />));
    expect(elem).toMatchSnapshot();
  });
});

このような手順を経ることで、意図した通りにコンポーネントレンダリングされているかをテストすることが出来る。

スナップショットファイルの中身

exports[describeの名前 testの名前 番号`] = `
レンダリングの内容
`;

番号は連番になっておりtestの中でtoMatchSnapshot()が呼ばれる度に番号が振られていく。

スナップショットファイルの再構築

テスト実行時に-- -uというオプションを付けることで、今回のテストの内容でスナップショットを全て書き換えてくれる。

クリックイベントと組み合わせる

応用編として、前回学んだクリックイベントのテストと組み合わせてみる。

まず、テストの対象となるコードを書く。

// dom2.js
import React from 'react';

class Count extends React.Component{
  constructor(props){
    super(props);
    this.state = {counter:0};
  };
  render(){
    return(
      <div>
        <div>{this.state.counter}</div>
        <button onClick={()=>{
          this.setState({counter: this.state.counter+1});
        }}>Click!</button>
      </div>
    );
  };
};

export { Count };
// dom2-render.js
import React from 'react';
import ReactDOM from 'react-dom';
import {Count} from './dom2.js';

ReactDOM.render(<Count />, document.getElementById('mount'));

こうすると画面には数値(初期値は0)とボタンが表示され、ボタンをクリックする度に数値の数が増えていく。

このコードを、テストしていく。

import React from 'react';
import { mount } from 'enzyme';
import { mountToJson } from 'enzyme-to-json';
import { Count } from '../dom2.js';

describe('domのテスト', () => {
  test('Countのテスト', () => {
    const elem = mount(<Count />);
    let currentDom = mountToJson(elem);
    expect(currentDom).toMatchSnapshot();
  });
});

上記のコードを書くと、以下のスナップショットが作成される。

exports[`domのテスト Countのテスト 1`] = `
<Count>
  <div>
    <div />
    <button
      onClick={[Function]}>
      Click!
    </button>
  </div>
</Count>
`;

<div>0</div>となるべきところが、<div />となっている。0は省略されるようだ。
このスナップショットを次のように書き換える。

exports[`domのテスト Countのテスト 1`] = `
<Count>
  <div>
    <div>
      1
    </div>
    <button
      onClick={[Function]}>
      Click!
    </button>
  </div>
</Count>
`;

exports[`domのテスト Countのテスト 2`] = `
<Count>
  <div>
    <div>
      2
    </div>
    <button
      onClick={[Function]}>
      Click!
    </button>
  </div>
</Count>
`;

クリックする度に数値が増えることをテストしている。先程のテストコードを実行すると、当然、通らない。
そこで、テストを以下のように書き換える。

import React from 'react';
import { mount } from 'enzyme';
import { mountToJson } from 'enzyme-to-json';
import { Count } from '../dom2.js';

describe('domのテスト', () => {
  test('Countのテスト', () => {
    const elem = mount(<Count />);
    elem.find('button').simulate('click');
    let currentDom = mountToJson(elem);
    expect(currentDom).toMatchSnapshot();

    elem.find('button').simulate('click');
    currentDom = mountToJson(elem);
    expect(currentDom).toMatchSnapshot();
  });
});

toMatchSnapshot()を2回行っているが、それぞれ、スナップショットと一致し、テストはパスする。

上記の結果から、テストのなかでレンダリングしたコンポーネントに対してイベントを起こせること、それによる表示の変更も問題なく行われていることが、確認できる。
また、mountToJson()toMatchSnapshot()を使うことで、その変化をテストできることが分かった。


イベントやDOMのテストは苦手だったのだが、それが出来るようになったことで、かなり視界が晴れた感じがする。

実際に使うことでこそ身に付くので、今後の開発で積極的にJestを使っていきたい。

参考資料

Facebook製のJavaScriptテストツール「Jest」の逆引き使用例 - Qiita