3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ReactとReduxのテストコードを書く

Last updated at Posted at 2021-04-22

##はじめに
これは忘れないために残したメモのため、あまり参考にならないかもしれません。
そのためあまり当てにならないかもしれません。ご自身で実行して確認してみてください。
検索して見つけれれるメモとして利用予定です。

とりあえず下記のように覚えた!
Jestは、JavaScriptの全般的なテストに使う。
@testing-library/reactは、Reactをテストするため最適化されたライブラリ。
※ create-react-appでReactプロジェクトを作るとどちらもインストールされるので、
今回は、Jest + react-testing-library の組み合わせを使用する

##環境
Windows10

##バージョン

> yarn --version 
1.22.10

> node --version
v14.14.0

##jestを使う準備
create react-appでプロジェクトを作成すると同梱されているらしい。準備は簡単!!

> yarn create react-app my-app --template typescript
yarn create v1.22.10
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Installed "create-react-app@4.0.3" with binaries:
      - create-react-app
"C:\Program" は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
error Command failed.
Exit code: 1

え。。
Nodistのインストール先がをデフォルトのC:\Program Files (x86)配下になっていたため、
なんか半角スペースが入ってしまうようで、正しくcreate react-appが実行できない。
c直下にインストールしなおして実行
ええ。。。
yarnをインストールしなおす。
下記は、nodeのバージョン12までじゃないと実行エラーになるが、yarn create react-app実行時は、nodeのバージョン10.12.0 または 12.0.0 以上にしなさいと言われる。

> npm install --global yarn

成功!

> yarn create react-app my-app --template typescript
・・・
Success! Created my-app at C:\pleiades\workspace_jest\my-app
Inside that directory, you can run several commands:

  yarn start
    Starts the development server.

  yarn build
    Bundles the app into static files for production.

  yarn test
    Starts the test runner.

  yarn eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd my-app
  yarn start

Happy hacking!
Done in 81.70s.

##初めてjestを使う
(ようやく来た!)
この状態で yarn testを実施。
はい、成功!!
自動で作られているsrc/App.test.tsxのテストが実行される。

> yarn test
yarn run v1.22.10
$ react-scripts test
 PASS  src/App.test.tsx (28.644 s)
  √ renders learn react link (63 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        51.465 s

##テストコードを自分で書く
###テストコード
デフォルトでは下記の二つがテストコードとして認識される。(少なくとも自分の環境では)
1.__tests__フォルダ配下に配置
2.○○.test.tsx○○.spec.tsx ファイル名に.testなどを入れる ※.ts|.tsx|.js
jest.config.jsに別のマッチ条件を書くこともできる

###テスト内容は、ボタンのクリックイベント確認と表示確認

import React from "react";
import { render, screen, fireEvent  } from "@testing-library/react";
import { Button } from "./Button"; // テスト対象のオリジナルボタンコンポーネント

describe("ボタン", () => {
    test("表示", () => {
        const testMock = jest.fn();
        render(<Button onClick={testMock}>testButton</Button>);
        const button = screen.getByText("testButton");
        screen.debug();
        // ラベル"testButton"が画面に表示されたことを確認
        expect(button).toBeInTheDocument();
    });

    test("onClick", () => {
        const testMock = jest.fn();
        render(<Button onClick={testMock}>testButton</Button>);
        const button = screen.getByText("testButton");
        fireEvent.click(button);
        // クリックイベントのファンクションが動くことを確認
        expect(testMock).toHaveBeenCalled();
    });
});

####上記内容の解説
・react-testing-libraryのrender関数
 テスト対象をレンダリングしテスト内でアクセス可能とできる。

・react-testing-libraryのdebug関数
 コンソール上にrenderで表示したHTML構造が表示される。取得可能な要素が分からない時に使える。

・react-testing-libraryのscreen.getByText関数
 レンダリングした画面から引数に渡したテキストの要素を探して取得する。

・react-testing-libraryのfireEvent.click関数
 引数にボタン要素を渡すとレンダリング中のボタン要素をクリックできる。

####その他使い方メモ
screen.get*系
 引数で指定した要素が存在しないとエラーとなる。

screen.query*系
 引数で指定した要素が存在なくともエラーとならない。※存在しない確認に使える。

screen.getByText("探したいテキスト")
 表示中の要素から引数で指定したテキストの要素を探す。

screen.queryByText("")screen.getByText("")
 コンソール上に表示中のHTML構造が表示される。※debug()と違いエラーにはなるが、、

screen.getByRole("")
 下記のように親切に指定できる引数と取得対象を教えてくれる。

・・・
--------------------------------------------------
  button:  ← これが引数に指定できる 例)screen.getByRole("button");

  Name "testButton":
  <button
    class="button"
  />

--------------------------------------------------
・・・

 ・beforeAll すべてのテストが実行される前1回だけ実行される。例)fetchMockをリセットなどに使用
 ・afterAll すべてのテストが実行された後1回だけ実行される。
 ・beforeEach テストが実行される前に毎回実行される。

beforeEach(() => {
 ~ 内容 ~
});

 ・afterEach テストが実行された後に毎回実行される。

##Hooks(useDispatch, useSelector)をモック化してテストコードを自分で書く
###テスト概要
あ、useDispatchuseSelectorを使用しているオリジナルのMadaiDialogをテストするという想定のテスト
importの下あたりに書いておく

###まずこれ

// useDispatchのモック
const mockDispatch = jest.fn();
// useSelectorのモック
const mockSelector = jest.fn();
jest.mock("react-redux", () => ({
    useDispatch: () => mockDispatch,
    useSelector: () => mockSelector(),
}));

###んで、上記の後にテストを書く

・・・
describe("ダイアログ表示テスト", () => {
  // 実行前にstateの内容を自分で設定 
    beforeEach(() => {
        // モックの内容設定
        mockSelector.mockImplementation(() => ({
            contentMessage: [], // ← useSelector時に取得したいstateの内容を自由に設定
            errorCode: null,    // ← useSelector時に取得したいstateの内容を自由に設定
        }));
    });

    test("表示", () => {
        render(<MadaiDialog open={true} />);
        const content = screen.queryByText("マダイ釣り教えます");
        // 表示されたことを確認
        expect(content).toBeInTheDocument();
    });

    test("非表示", () => {
        render(<MadaiDialog open={false} onSelect={testMock}/>);
        const content = screen.queryByText("マダイ釣りの方法");
        // 表示されていないことを確認
        expect(content).not.toBeInTheDocument();
    });
});
・・・

####上記内容の解説と特徴
describe毎にモックの内容を変えたいテスト!!!

MadaiDialogコンポーネント内でuseDispatchuseSelectorを使っているため、モックが必要!!

queryByTextを使用して.notで表示されていないことを確認している!!!!
 ※getByTextだとエラーとなるよ。
 ※表示の有無は、open={false}の設定で決まる使用のコンポーネントのため、モックを確認するテストとなっていない。
 ※モックを確認するテストは、下記。

###続いてモックを確認するテスト書く

・・・
describe("モックのテスト", () => {
    beforeEach(() => {
        // モックの内容設定
        mockSelector.mockImplementation(() => ({
            contentMessage: ["マダイ釣り教えます!!表示されていますでしょうか??" , "マダイ釣り教えます2!!"],
        }))
    })

    test("contentMessage 表示", () => {
        render(<MadaiDialog open={true} />);
        const content = screen.getByText("マダイ釣り教えます!!表示されていますでしょうか??");
        const content2 = screen.getByText("マダイ釣り教えます2!!");
        // 表示されたことを確認
        expect(content).toBeInTheDocument();
        expect(content2).toBeInTheDocument();
    });

    test("contentMessage 非表示", () => {
        render(<MadaiDialog open={false} />);
        const content = screen.queryByText("マダイ釣り教えます!!表示されていますでしょうか??");
        const content2 = screen.queryByText("マダイ釣り教えます2!!");
        // 表示されていないことの確認
        expect(content).not.toBeInTheDocument();
        expect(content2).not.toBeInTheDocument();
    });
});
・・・

####上記内容の解説と特徴
・モックで設定したメッセージが画面に表示されているかのテストとなっている。

・二つ目は、open={false}を設定しダイアログが非表示状態にしているため、メッセージが非表示となっている確認をしている。

####その他使い方メモ
mockReturnValueOnce
 useSelectorを呼び出したときに返す値を引数に受け取る。複数つなげて呼び出し回数に応じて返却値が順に変えられる。

mockSelector
 .mockReturnValueOnce(1)  // ← 1回目のuseSelectorの時に1が買える 
 .mockReturnValueOnce(2)  // ← 2回目のuseSelectorの時に2が買える
 .mockReturnValueOnce(3); // ← 3回目のuseSelectorの時に3が買える

mockImplementationOnce
 useSelectorを呼び出したときに使用される関数を引数に受け取る。複数つなげて呼び出し回数に応じて実行関数を順に変えられる。

###続いてクリック動作による分岐表示のテスト書く
メッセージをクリックするとローカルstateが変更され、メッセージが消えて
マダイ釣りの極意(詳細内容)が表示される仕様のコンポーネントという想定のてすと

・・・
describe("クリックのテスト", () => {
    beforeEach(() => {
        // モックの内容設定
        mockSelector.mockImplementation(() => ({
            contentMessage: ["マダイ釣り教えます!!表示されていますでしょうか??""],
            contentDetail: ["マダイ釣りの極意その1・・・・・・"],
        }))
    })

    test("クリックしていないた場合", () => {
        render(<MadaiDialog open={true} />);
        const content = screen.queryByText("マダイ釣り教えます!!表示されていますでしょうか??");
        const contentDetail = screen.queryByText("マダイ釣りの極意その1・・・・・・");

        // 表示されていることを確認
        expect(content).toBeInTheDocument();
        // 表示されていないことを確認
        expect(contentDetail).not.toBeInTheDocument();
    });

    test("クリックした場合", () => {
        render(<MadaiDialog open={true} />);
        const content = screen.queryByText("マダイ釣り教えます!!表示されていますでしょうか??");
        fireEvent.click(content);
        const contentDetail = screen.queryByText("マダイ釣りの極意その1・・・・・・");

        // 表示されていないことを確認
        expect(content).not.toBeInTheDocument();
        // 表示されていることを確認
        expect(contentDetail).toBeInTheDocument();
    });
});
・・・

####上記内容の解説と特徴
・クリックしたときに表示内容が変わる仕様をテストしている。
その他は特に変わりなし

##ついでにActionのテストコードを自分で書く
メモだしな、、記事を分けるのもなあと思い。。ここに書きます!。

###テスト概要
APIのレスポンスをfetch-mockを使ってモック化し、redux-mock-storeを使って評価するテスト
※あんまりよくわかっていない。
※とりあえず動かして詰まったところを紹介する

####とりあえず書いたやつ

import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import fetchMock from "fetch-mock";
import { fetchContents } from "./action"; // ← テスト対象のアクション

const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

describe("action", () => {

    afterEach(() => {
        // テストを実行毎にfetchMockをリセット
        fetchMock.restore();
    });

    it("fetchContents SUCCESS", () => {

        // fetchAPIのmock化
        // これを使うためにnode-fetchを追加
        // https://github.com/wheresrhys/fetch-mock/blob/master/docs/cheatsheet.md
        fetchMock.postOnce("path:/api/fetchContents/getlist", {
            headers: { "content-type": "application/json" },
            body: {
                "contentMessage": [
                    "マダイ釣り教えます!!表示されていますでしょうか??" ,
                    "マダイ釣り教えます2!!"
                ]
            },
        });

        // 確認用
        const expectedActions = [
            {
                type: "MADAI_LIST",
                payload: {},
            },
            {
                type: "MADAI_LIST_SUCCESS",
                payload: {
                    contentMessage: [
                        "マダイ釣り教えます!!表示されていますでしょうか??" ,
                        "マダイ釣り教えます2!!"
                    ]
                },
            },
        ];

        const store = mockStore({});
        return store.dispatch(fetchSuggestionNeeds(projectId))
            .then(() => {
                // console.log(JSON.stringify(store.getActions())); // 結果出力
                expect(store.getActions()).toEqual(expectedActions);
            });
    });

    it("fetchContents FAILURE", () => {
        fetchMock.postOnce("path:/api/fetchContents/getlist", {
            headers: { "content-type": "application/json" },
            body: {
                error: {
                    code: "ERROR_TEST",
                    message:"test_message",
                }
            },
            status: 500,
        });

        ~ 省略 ~
    });
});

####上記内容の解説と特徴

fetch-mockを使うためにnode-fetchを追加している
・テストごとにモックをリセットしている。同じパスで設定できるのは一つのようなので、リセットが必要ぽい
postOnce指定したパスに該当するAPIレスポンスをモック化している
※path指定のほかにもいろいろ指定方法があるようです。
・後は、APIレスポンスを受け取って加工された結果を評価している

##アプリケーションテストの考え方
https://blog.recruit.co.jp/rtc/2020/07/15/reactredux_app_test_strategy/

テストによって何が保証できているかを考える必要があります

なるほど、どこまでテストするのか決めて合わせないといけないのか

##最後に
最後まで見てくれたみんなありがとうございます!!
ご覧になって、いかがでしょうか?
初投稿にしては、なかなか良い記事となったと思います。
それでは、またいつか記事を書くその時に(^_^)ノ

調べる時間をくれて、ありがとう!!

3
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?