Open10

Reactテスト手法 調査

omisonosoupomisonosoup

作業用プロジェクトを作成。

$  npx create-react-app demo

とりあえずテストを動かしてみる。

$ cd demo
$ yarn test

以下のように出力される。変更がない場合はテストは行われない。

No tests found related to files changed since last commit.
Press `a` to run all tests, or run Jest with `--watchAll`.

Watch Usage: Press w to show more.

適当にApp.jsを編集する

App.js
import logo from './logo.svg';
import './App.css';

function App() {
  return <div className='App'></div>;
}

export default App;

テストに失敗する

 FAIL  src/App.test.js
  ✕ renders learn react link (29 ms)

  ● renders learn react link

    TestingLibraryElementError: Unable to find an element with the text: /learn react/i. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

    <body>
      <div>
        <div
          class="App"
        />
      </div>
    </body>

      4 | test('renders learn react link', () => {
      5 |   render(<App />);
    > 6 |   const linkElement = screen.getByText(/learn react/i);
        |                              ^
      7 |   expect(linkElement).toBeInTheDocument();
      8 | });
      9 |

      at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:37:19)
      at node_modules/@testing-library/dom/dist/query-helpers.js:90:38
      at node_modules/@testing-library/dom/dist/query-helpers.js:62:17
      at getByText (node_modules/@testing-library/dom/dist/query-helpers.js:111:19)
      at Object.<anonymous> (src/App.test.js:6:30)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        3.261 s
Ran all test suites related to changed files.

Watch Usage: Press w to show more.

learn reactの文字を戻す

App.js
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className='App'>
      <header className='App-header'>
        <a
          className='App-link'
          href='https://reactjs.org'
          target='_blank'
          rel='noopener noreferrer'
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

テスト成功

 PASS  src/App.test.js
  ✓ renders learn react link (22 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.031 s
Ran all test suites related to changed files.

Watch Usage: Press w to show more.
omisonosoupomisonosoup

App.test.jsのようにtestがついているものがテストに使用される

App.test.js
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

testの中でErrorが起きるとテストに失敗する。

omisonosoupomisonosoup

Shallow Renderingとは1階層だけ描写するもの

Shallow
<div id="sample-form">
  <p>sample</p>
  <InputComponent />
  <SubmitComponent />
</div>
Mount
<div id="sample-form">
  <div>
    <p>sample</p>
    <input type="text">
  </div>
  <button type="submit">
    Submit
  </button>
</div>

テスト対象を分離させるのに良い

omisonosoupomisonosoup

Enzymeを使って超基本的なテストを書いてみる

App.test.js
import Enzyme, { shallow } from 'enzyme';
import EnzymeAdapter from '@wojtekmaj/enzyme-adapter-react-17';

import App from './App';

Enzyme.configure({ adapter: new EnzymeAdapter() });

test('renders component with enzyme.', () => {
  const wrapper = shallow(<App />);
  expect(wrapper.exists()).toBe(true);
});
omisonosoupomisonosoup

テスト駆動開発をする。

先に期待する内容を列挙する。

App.test.js
import App from './App';
import Enzyme, { shallow } from 'enzyme';
import EnzymeAdapter from '@wojtekmaj/enzyme-adapter-react-17';

Enzyme.configure({ adapter: new EnzymeAdapter() });

test('renders withou error', () => {
  // テストの内容
});
test('renders button', () => {});
test('renders text', () => {});

その後、テストの内容を書く。

App.test.js
import App from './App';
import Enzyme, { shallow } from 'enzyme';
import EnzymeAdapter from '@wojtekmaj/enzyme-adapter-react-17';

Enzyme.configure({ adapter: new EnzymeAdapter() });

test('renders withou error', () => {
  const wrapeer = shallow(<App />);
  const appComponent = wrapeer.find("[data-test='component-app']");
  expect(appComponent.length).toBe(1);
});

この時点ではエラーが起きるのでテストをパスするように実装を行う。

App.js
import './App.css';

function App() {
  return <div data-test='component-app'></div>;
}

export default App;
omisonosoupomisonosoup

date-test='component-app'のようなテストのために用意した実質不要なプロパティを公開時に取り除く方法

まず必要なプラグインをインストールする。

$ yarn add -D babel-plugin-react-remove-properties

細かい設定が行えるようにejectする

$ yarn eject

yを選択する。

package.jsonのbabelに設定を追加

  "babel": {
    "env": {
      "production": {
        "plugins": [
          ["react-remove-properties", {"properties": ["data-test"]}]
        ]
      }
    },
    "presets": [
      "react-app"
    ]
  }

buildを行い、実行してみる。

$ yarn build
$ npm install -g serve
$ serve -s build

ブラウザの開発者ツールで確認するとdata-testプロパティは消えた状態でページが表示される。

omisonosoupomisonosoup

DRY: Don't Repeat Yourself

多用する箇所はfunction化すると良い

App.test.js
Enzyme.configure({ adapter: new EnzymeAdapter() });

// ↓こんな感じで説明を書くと親切

/**
 * Factory function to create a Shallow Wrapper for the App component.
 * @function setUp
 * @returns {ShallowWrapper}
 */
const setUp = () => shallow(<App />);

const findByTestAttr = (wrapper, val) => wrapper.find(`[data-test='${val}']`);

test('renders withou error', () => {
  const wrapper = setUp();
  const appComponent = findByTestAttr(wrapper, 'component-app');
  expect(appComponent.length).toBe(1);
});

ただしテストとして意味を持つように繰り返すのはOK
テストは時にドキュメントのような役目を持つ。

omisonosoupomisonosoup

テキストの内容をテストする、ボタンを押すことを想定するなどの書き方は以下

App.test.js
test('counter display starts at 0', () => {
  const wrapper = setUp();
  const count = findByTestAttr(wrapper, 'count').text();
  expect(count).toBe('0');
});
test('clicking button increments counter', () => {
  const wrapper = setUp();
  // ボタンを見つける
  const button = findByTestAttr(wrapper, 'increment-button');
  // ボタンを押す
  button.simulate('click');
  // カウンターを見つける
  const count = findByTestAttr(wrapper, 'count').text();
  // 数値が1になっているはず
  expect(count).toBe('1');
});

最初に一度に要素を取得するのではなく、変化後をテストするのであれば都度要素を発見するようにする。