FORCIA CUBEフォルシアの情報を多面的に発信するブログ

テストしやすい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");
});



最後に

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

この記事を書いた人

籏野 拓

2018年新卒入社。
自動化大好きエンジニアを目指します

フォルシアではフォルシアに興味をお持ちいただけた方に、社員との面談のご案内をしています。
採用応募の方、まずはカジュアルにお話をしてみたいという方は、お気軽に下記よりご連絡ください。


採用お問い合わせフォーム 募集要項

※ 弊社社員に対する営業行為などはお断りしております。ご希望に沿えない場合がございますので予めご了承ください。