JestでReactアプリのテストを行う

前章でReactを使ってTodoアプリを作成しました。今回はJestEnzymeを使って、前章で作ったTodoアプリのテストを記述していきます。

目次

Jestとは

jest logo

JestはFacebook製のテストツールであり、ReactアプリではよくJestがテストツールとして用いられます。Create React App を使ってプロジェクトを構築した場合、既にJestが設定された状態でプロジェクトが生成されます。今回はこの Jest を用いて、前章で作ったTodoアプリのテストを記述していきましょう。

Jestは実はCodeSandboxなどのWebサービス上でも動作させることができます。

CodeSandboxでテストを走らせてみる

jest logo

Enzymeとは

EnzymeAirbnbによって開発されているReactコンポーネントのテストツールです。通常、React v16で使用するには、enzyme本体に加えてadapterが必要なので、 enzyme-adapter-react-16を併せてインストールします。

npm install --save-dev enzyme
npm install --save-dev enzyme-adapter-react-16

そして、adapterを下記のように適用します。

import React from "react";
import Enzyme from "enzyme";
import { mount } from "enzyme";

// React v16用のアダプターをインストール
import Adapter from "enzyme-adapter-react-16";

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

describe()とtest()

Jestではdescribe() 関数で test() 関数テストのグルーピングを行うことができます。関連したテスト群を一つのdescribe() 関数を記述して具体的なテスト内容の test() 関数の中に記述します。そして expect() 関数とマッチャーを使って値が予期する値となっているかを確認します。

Expect

const myBeverage = {
  delicious: true,
  sour: false,
};

describe('my beverage', () => {
  test('is delicious', () => {
    // myBevarage.deliciousがtruthyであれば
    expect(myBeverage.delicious).toBeTruthy();
  });

  test('is not sour', () => {
    expect(myBeverage.sour).toBeFalsy();
  });
});

TodoContextのテストを記述する

TodoContext.jsxのテストを記述していきます。TodoContextではStateの管理を行っています。初期状態のStateの確認と各種メソッド呼び出しで、Stateが正常に更新されるかどうかを確認しています。

import React from "react";
import Enzyme from "enzyme";
import { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

import { TodoContextProvider } from "./TodoContext";

// React v16からadapterを適用する必要があります。
// https://airbnb.io/enzyme/docs/installation/#working-with-react-16
Enzyme.configure({ adapter: new Adapter() });

// TodoContextProviderをマウント
const mountTodoContextProvider = () => shallow(<TodoContextProvider />);

describe("<TodoInputDialog />", () => {
  it("初期Stateが正しい", () => {
    const wrapper = mountTodoContextProvider();
    const state = wrapper.state();

    // State
    expect(state.todoInputDialogShow).toBe(false);
    expect(state.todoTitleInput).toBe("");

    // Methods
    expect(typeof state.addTodo).toBe("function");
    expect(typeof state.deleteTodo).toBe("function");
    expect(typeof state.toggleDialog).toBe("function");
    expect(typeof state.updateTitleInput).toBe("function");
    wrapper.unmount();
  });

  it("Todoが正しく追加される", () => {
    const wrapper = mountTodoContextProvider();
    wrapper.instance().addTodo({ title: "MyTodo" });
    const state = wrapper.state();
    expect(state.todos[1].title).toBe("MyTodo");
    wrapper.unmount();
  });

  it("Todoが正しく削除される", () => {
    const wrapper = mountTodoContextProvider();
    const targetId = wrapper.state().todos[0].id;
    wrapper.instance().deleteTodo(targetId);
    expect(wrapper.state().todos.length).toBe(0);
    wrapper.unmount();
  });

  it("ダイアログの表示・非表示が正しく更新される", () => {
    const wrapper = mountTodoContextProvider();
    wrapper.instance().toggleDialog();
    expect(wrapper.state().todoInputDialogShow).toBe(true);
    wrapper.unmount();
  });

  it("Todoの追加テキストが正しく更新される", () => {
    const wrapper = mountTodoContextProvider();
    wrapper.instance().updateTitleInput("Hello");
    expect(wrapper.state().todoTitleInput).toBe("Hello");
    wrapper.unmount();
  });
});

AddTodoBtnのテストを記述する

AddTodoBtnのテストを記します。AddTodoBtnでは正常に要素がレンダリングされることと、ボタンがクリックされた時に、Stateが正しく更新されることを確認します。

import React from "react";
import Enzyme from "enzyme";
import { mount } from "enzyme";

// React v16用のアダプターをインストール
import Adapter from "enzyme-adapter-react-16";

import { TodoContextProvider } from "./TodoContext";
import { AddTodoBtn } from "./AddTodoBtn";

// React v16からadapterを適用する必要があります。
// https://airbnb.io/enzyme/docs/installation/#working-with-react-16
Enzyme.configure({ adapter: new Adapter() });

// TodoContextProvider, AddTodoBtnをマウント。
const mountAddTodoBtn = () =>
  mount(
    <TodoContextProvider>
      <AddTodoBtn />
    </TodoContextProvider>
  );

describe("<AddTodoBtn />", () => {
  it("AddTodoBtnが正常にレンダリングされる", () => {
    const wrapper = mountAddTodoBtn();
    expect(wrapper.find("button.spec-add-todo-btn").length).toBe(1);
  });

  it("AddTodoBtnクリック時にtodoInputDialogShowが切り替わる", () => {
    const wrapper = mountAddTodoBtn();
    const closeButton = wrapper.find("button.spec-add-todo-btn");
    closeButton.simulate("click");
    expect(wrapper.state().todoInputDialogShow).toBe(true);
    closeButton.simulate("click");
    expect(wrapper.state().todoInputDialogShow).toBe(false);
  });
});

TodoInputDialogのテストを記述する

TodoInputDIalogのテストを記述します。TodoInputDialogが正常にレンダリングされ、Todoが正しく追加されることを確認します。

import React from "react";
import Enzyme from "enzyme";
import { mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

import { TodoContextProvider } from "./TodoContext";
import { TodoInputDialog } from "./TodoInputDialog";

// React v16からadapterを適用する必要があります。
// https://airbnb.io/enzyme/docs/installation/#working-with-react-16
Enzyme.configure({ adapter: new Adapter() });

const mountTodoInputDialog = () =>
  mount(
    <TodoContextProvider>
      <TodoInputDialog />
    </TodoContextProvider>
  );

describe("<TodoInputDialog />", () => {
  it("TodoInputDialogが正しくレンダリングされる。", () => {
    const wrapper = mountTodoInputDialog();
    wrapper.instance().toggleDialog();
    wrapper.instance().updateTitleInput("Hello");
    wrapper.update();
    expect(wrapper.find("input.spec-title-input").props().value).toBe("Hello");
  });

  it("Todoが正しく追加される。", () => {
    const wrapper = mountTodoInputDialog();
    wrapper.instance().toggleDialog();
    wrapper.instance().updateTitleInput("Hello");
    wrapper.update();
    const addButton = wrapper.find("button.spec-dialog-btn");
    addButton.simulate("click");
    expect(wrapper.state().todos.length).toBe(2);
  });
});

TodoListのテストを記述する

TodoListがレンダリングされ、TodoListが更新されることを確認します。

import React from "react";
import Enzyme from "enzyme";
import { mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

import { TodoContextProvider } from "./TodoContext";
import { TodoList } from "./TodoList";

// React v16からadapterを適用する必要があります。
// https://airbnb.io/enzyme/docs/installation/#working-with-react-16
Enzyme.configure({ adapter: new Adapter() });

const mountTodoList = () =>
  mount(
    <TodoContextProvider>
      <TodoList />
    </TodoContextProvider>
  );

describe("<TodoList />", () => {
  it("TodoListが正しくレンダリングされる。", () => {
    const wrapper = mountTodoList();

    expect(wrapper.state().todos.length).toBe(1);
    expect(wrapper.find("li.spec-list-item").length).toBe(1);
    expect(wrapper.text()).toBe("1st Todo");
  });

  it("TodoListが正しく更新される。", () => {
    const wrapper = mountTodoList();

    const closeButton = wrapper.find("svg.spec-close-btn");
    closeButton.simulate("click");
    expect(wrapper.state().todos.length).toBe(0);
    expect(wrapper.find("li.spec-list-item").length).toBe(0);
  });
});