😸

テストしやすいselectorsを求めて

2022/06/07に公開

概要

こんにちは、エンジニアの籏野です。

フォルシアのフロント開発ではReduxを利用して状態管理をしていることが多いです。
その中で、selectorsの書き方について少々気になったことがあったので紹介したいと思います。

背景

現在開発を進めているプロジェクトではRedux周りのディレクトリ構成にre-ducksパターンを採用しています。
※re-ducksパターンについては詳しい記事がたくさんあるので詳しい説明は省きます。

re-ducksパターンに沿ってコードを書いているとselectorsを定義すると以下のようにしている方が多いのではないでしょうか。

// selectors.ts
import { createSelector } from "@reduxjs/toolkit";

const stateSelector = (state) => state.hoge;
export const fugaSelector = createSelector(stateSelector, hoge => hoge.fuga);

selectorsをテストしたい場合は、テスト用に作成したstateを渡せばいいので比較的簡単に書けます。

// selectors.test.ts
import { fugaSelector } from "./selectors"

const testState = {
    hoge: {
        fuga: "test"
    }
};

it("fugaの値を取得する", () => {
    expect(fugaSelector(testState)).toBe("test");
});

ここまでは簡単なのですが、コンポーネントやhooksのテストを実装するときにあまりきれいに書けずにもやもやしてしまいました。
例えば以下のようなhooks関数をテストする場合を考えます。

// hooks.ts
import { useSelector } from "react-redux";
import { fugaSelector } from "path/to/selectors"

export const useProps = () => {
    const hoge = useSelector(fugaSelector);
    
    return { hoge }
};

useSelectorはReduxのStateに依存しているため、useSelectorのモックを作成してテストを書きます。

// hooks.test.ts
import { renderHook } from "@testing-library/react-hooks";
import { useProps } from "./hooks";

const useSelectorMock = jest.fn();
jest.mock("react-redux", () => {
    return {
        useSelector: () => useSelectorMock()
    };
})

it("propsを取得", () => {
    useSelectorMock.mockReturnValue("test");
    const { result } = renderHook(() => useProps());
    expect(result.current).toStrictEqual({ hoge: "test" });
});

こちらでテストは問題なく通ります。

ただ、usePropsで複数のselectorを呼びだす場合はどうでしょうか?
例えば以下のように mockReturnValueOnce を利用する方法も考えられますが、コードの書き方に依存しすぎていて保守性が低くなることが予見されます。

// 略
it("propsを取得", () => {
    useSelectorMock
        .mockReturnValueOnce("test")
        .mockReturnValueOnce("piyo")
        ....;
    const { result } = renderHook(() => useProps());
    expect(result.current).toStrictEqual({ hoge: "test", piyo: "piyo", ... });
});

どのように変えたか

最終的に、selectors.tsでuseSelectorをラップしたhooks関数を用意するのが一番きれいだろうという結論に至りました。

// selectors.ts
import { createSelector } from "@reduxjs/toolkit";

const stateSelector = (state) => state.hoge;
const fugaSelector = createSelector(stateSelector, hoge => hoge.fuga);
export const useFugaSelector = () => useSelector(fugaSelector);

hooks関数でのテストでは useFugaSelector をモックすればいいので、より直感的でわかりやすくなりました。

// 略
it("propsを取得", () => {
    useFugaSelectorMock.mockReturnValue("test");
    usePiyoSelectorMock.mockReturnValue("test");
    ....;
    const { result } = renderHook(() => useProps());
    expect(result.current).toStrictEqual({ hoge: "test", piyo: "piyo", ... });
});

selectors自体のテストはuseSelectorをモックすることで、これまでと似た形で実行できます。
※fugaSelectorをexportしてこれまで通りテストをすることも考えられますが、テスト目的以外でfugaSelectorを利用することは考えられなかったのでこの形にしています。

import { renderHook } from "@testing-library/react-hooks";
import { useFugaSelector } from "./selectors";

jest.mock("react-redux", () => {
	return {
		useSelector: jest.fn(selector => selector(testState))
	};
});
const testState = {
	hoge: {
		fuga: "test"
	}
};
it("fugaの値を取得する", () => {
	const { result } = renderHook(() => useFugaSelector());
	expect(result.current).toBe("test");
});

最後に

開発に集中しているとテストの書きやすさを忘れがちですが、今回のような知見を少しずつ貯めていってエンジニアとして強くなっていきたいです。

FORCIA Tech Blog

Discussion