LoginSignup
0
0

More than 1 year has passed since last update.

Vuex4のモックが楽だった話

Last updated at Posted at 2022-07-10

小一時間、どうすればモック出来るんこれ?と悩んだものの結果的に簡単にモックできたのでメモ
最終的なテストコードは真ん中あたりにあります。

対象のモジュール

  • サンプルはローディング状態を管理するだけの簡単なモジュール
  • actions/gettersのTYPOに悩まされたくないので、定数管理する都合上namespacedは切っていません
    • 呼び出し時にprefixを付けるか、定義時にprefixを取り除くかする必要があるため
  • PATHの解決に@を使うのが好みではないため、相対PATHにしています
import { ActionContext, Module, MutationPayload, Payload } from 'vuex';
import { RootState } from '../..';

const MODULE_NAME = 'ui/loading';

export type State = {
  loading: boolean;
};

// mutators
export const SET_LOADING = `${MODULE_NAME}/setState` as const;

// actions
export const LOADING_ACTION = `${MODULE_NAME}/loading` as const;
export const LOADED_ACTION = `${MODULE_NAME}/loaded` as const;

// getters
export const GET_LOADING = `${MODULE_NAME}/getLoading` as const;

// mutator payloads
export interface SetLoadingPayload extends MutationPayload {
  type: typeof SET_LOADING;
  payload: boolean;
}

// action payloads
export interface LoadingActionPayload extends Payload {
  type: typeof LOADING_ACTION;
}
export interface LoadedActionPayload extends Payload {
  type: typeof LOADED_ACTION;
}

export const LoadingModule: Module<State, RootState> = {
  state: {
    loading: false,
  },
  mutations: {
    [SET_LOADING](state: State, { payload }: SetLoadingPayload): void {
      state.loading = payload;
    },
  },
  actions: {
    [LOADING_ACTION]({ commit }: ActionContext<State, RootState>): void {
      commit<SetLoadingPayload>({ type: SET_LOADING, payload: true });
    },
    [LOADED_ACTION]({ commit }: ActionContext<State, RootState>): void {
      commit<SetLoadingPayload>({ type: SET_LOADING, payload: false });
    },
  },
  getters: {
    [GET_LOADING]: (state: State): boolean => state.loading,
  },
};

export default LoadingModule;

本題

最初はstoreからactionsを取り出す方法とか模索していたが、そんな必要なかった。
結論から言うと、new Vuex.storeする際にモックした関数を食わせるだけで十分だった。

やりたかったこと

  • 意図した通りのmutation/action/getterが呼び出されていることを確認したい
  • 意図した通りのpayloadが渡されていることを確認したい
  • 第一引数はActionContextなので、ここではExpect.anything()を指定しておく
  • new Vuex.Storeする際に定義時のRootStateが参照されるせいかジェネリック型に出来なかったのは残念
  • 内部で分岐が発生するようなケースがあれば、storeを作り直すだけでOK

テストコード

import Vuex from 'vuex';
import Module, * as LoadingModule from '../loading';

describe('modules/ui/loading', () => {
  const setLoading = jest.fn();
  const loadingAction = jest.fn();
  const loadedAction = jest.fn();
  const getLoading = jest.fn();

  const store = new Vuex.Store({
    modules: {
      loading: {
        ...Module,
        actions: {
          ...Module.actions,
          [LoadingModule.LOADING_ACTION]: loadingAction,
          [LoadingModule.LOADED_ACTION]: loadedAction,
        },
        mutations: {
          ...Module.mutations,
          [LoadingModule.SET_LOADING]: setLoading,
        },
        getters: {
          ...Module.getters,
          [LoadingModule.GET_LOADING]: getLoading,
        },
      },
    },
  });
  const { dispatch, commit, getters } = store;

  describe('INITIAL STATE', () => {
    it('loading', () => {
      expect(store.state).toEqual({ loading: { loading: false } });
    });
  });

  describe('mutations', () => {
    it('SET_LOADING', () => {
      commit<LoadingModule.SetLoadingPayload>({ type: LoadingModule.SET_LOADING, payload: true });
      expect(setLoading).toHaveBeenCalledWith(expect.anything(), { type: LoadingModule.SET_LOADING, payload: true });
    });
  });

  describe('actions', () => {
    it('LOADING_ACTION_PAYLOAD', async () => {
      await dispatch<LoadingModule.LoadingActionPayload>({ type: LoadingModule.LOADING_ACTION });
      expect(loadingAction).toHaveBeenCalledWith(expect.anything(), { type: LoadingModule.LOADING_ACTION });
    });

    it('LOADED_ACTION_PAYLOAD', async () => {
      await dispatch<LoadingModule.LoadedActionPayload>({ type: LoadingModule.LOADED_ACTION });
      expect(loadedAction).toHaveBeenCalledWith(expect.anything(), { type: LoadingModule.LOADED_ACTION });
    });
  });

  describe('getters', () => {
    it('GET_LOADING', () => {
      getters[LoadingModule.GET_LOADING];
      expect(getLoading).toHaveBeenCalledTimes(1);
    });
  });
});

Vue3/Vuex4の所感

  • Piniaとかも出てるけど、Vue3/Vuex4でTypeScript対応はかなり楽に出来るなという印象
    • Vuexの方では特に困った覚えはない
  • 最初の方でも書いたがnamespaced: trueを指定すると定数管理してTYPOを防ごうという目論見が見事に崩れる
  • dispatchにPayloadの型を指定するとtypeを含んだPayloadWithTypeになってしまい、取り回しが悪くなってしまうのがとても残念
  • TypeScriptで書く上で非常に困ってるのが、script setup記法だとコンポーネントをジェネリック型に出来ない点
    • 解決策としてslotを使うという手段をとっているが、スマートじゃない
    • Githubにdiscussionsがあるので、そのうち実現しそうではある
    • そのためだけにdefineComponent使うのもなんか負けた気がする

ちなみにPiniaで書くとこうなる

対象ファイル

import { defineStore } from 'pinia';

type State = {
  loading: boolean;
};

export const uiLoadingStore = defineStore('ui/loading', {
  state: (): State => ({
    loading: false,
  }),
  actions: {
    start(): void {
      this.loading = true;
    },
    finish(): void {
      this.loading = false;
    },
  },
});

テスト

import { setActivePinia, createPinia } from 'pinia';
import { uiLoadingStore } from '../loading';

describe('ui/loading', () => {
  beforeEach(() => {
    setActivePinia(createPinia());
  });

  it('initial state', () => {
    const uiLoading = uiLoadingStore();
    expect(uiLoading.$state).toEqual({ loading: false });
  });

  describe('action', () => {
    it('start', () => {
      const uiLoading = uiLoadingStore();
      jest.spyOn(uiLoading, 'start');

      uiLoading.start();
      expect(uiLoading.start).toHaveBeenCalledWith();
    });

    it('finish', () => {
      const uiLoading = uiLoadingStore();
      jest.spyOn(uiLoading, 'finish');

      uiLoading.finish();
      expect(uiLoading.finish).toHaveBeenCalledWith();
    });
  });
});
0
0
0

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
0
0