Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Vuexのテストで別モジュールの呼び出しをしたい

More than 1 year has passed since last update.

前置き: Vuexストアの名前空間について

Vuexのstoreにおいて、ファイルが肥大化するのを防ぐためにモジュールを名前空間で分けることがあります。

その際、storeファイルで別モジュールのファイルを呼び出すときは「モジュール名/アクション名」で書くことができます。

(Nuxt.jsのモジュールモードで書いた例です。Vuexのみの場合は書き方が違う可能性があります。)

例: store/index.js

    export const actions = {
      async clientLogin({ commit, dispatch }, params) {
        try {
          //ログイン処理
        } catch (error) {
          //ログイン失敗時
          if (error.response.status == "401") {
            dispatch(
              //store/index.js内で、store/presentations内のshowAndCloseMessageアクションを呼び出す
              "presentations/showAndCloseMessage",
              {
                message: "IDもしくはパスワードに誤りがあります",
                isSuccessMessage: false
              },
              { root: true }
            );
          } else {
            throw new Error(error.response.status);
          }
        }
      }
    };
    { root: true }

この部分はグローバル名前空間にアクションをコミットするために必要です。

"presentations/showAndCloseMessage"はrootが起点となるので、dispatchの第3引数として{ root: true }が必要となります。

ここまでは公式ドキュメントにもある記述ですが、この処理をJestでテストする際にめちゃめちゃハマりました。

本題: テスト中での別モジュールのaction呼び出し

今回別モジュールのactionを呼び出すのは、indexストアのログイン処理の中です。

store/index.js

    export const state = () => ({
      token: null,
      ID: null
    });

    export const getters = {
      token: state => state.token,
      ID: state => state.ID
    };

    export const mutations = {
      setClient(state, { ID, token }) {
        state.ID = ID;
        state.token = token;
      }
    };

    export const actions = {
      async clientLogin({ commit, dispatch }, params) {
        try {
         //ログイン処理
        } catch (error) {
          if (error.response.status == "401") {
            //失敗時は別モジュールのactionを呼び出して、メッセージを表示する
            dispatch(
              //ここ!!
              "presentations/showAndCloseMessage",
              {
                message: "IDもしくはパスワードに誤りがあります"
              },
              { root: true }
            );
          } else {
            throw new Error(error.response.status);
          }
        }
      }
    };

この部分は、認証エラーでログイン失敗したときの処理です。
別モジュールであるpresentationsストアのshowAndCloseMessageアクションを呼び出して、フラッシュメッセージを一定時間表示するようにしています。

では実際にこの別モジュールのストアのstateの更新と、その確認をどう行ったらよいでしょうか?

下記のように書くことで別モジュールのアクションの発行とテストを行うことができました。
①〜⑤を順番に解説します。

stores/index.spec.js

    import { createLocalVue } from "@vue/test-utils";
    import Vuex from "vuex";

    //①使用するstoreをimportしておく
    import * as index from "@/store/index.js";
    import * as presentationsStore from "@/store/presentations.js";

    import { cloneDeep } from "lodash";
    import axios from "axios";
    const MockAdapter = require("axios-mock-adapter");
    const mock = new MockAdapter(axios);

    const localVue = createLocalVue();
    localVue.use(Vuex);

    //②ストアの汚染を防ぐためにcloneDeepでstoreをコピーする
    const store = new Vuex.Store(cloneDeep(index));
    const presentation_store = new Vuex.Store(cloneDeep(presentationsStore));

    store.$axios = axios;

    //③storeをモックする
    let presentations = {
      namespaced: true,
      actions: {
        showAndCloseMessage: ({}, arg) =>
          // actions内で他モジュールのactionをdispatchしている部分
          // "presentations/showAndCloseMessage"がdispatchされた時に内部でstore/presentations内のactionを呼ぶ
          presentation_store.dispatch("showAndCloseMessage", {
            message: arg.message
          })
      },
      state: {
        message: () => ""
      }
    };

    describe("クライアントアカウントでのログイン(clientLogin())", () => {
      //④store/index.jsからモックが呼び出せるようにします
      store.registerModule("presentations", cloneDeep(presentations));
      const loginClient = { id: "sample", pass: "TestUser0000" };

      it("clientLogin()の401エラー時にメッセージを表示する", async () => {
        mock.onPost("/api/client/login").reply(401);
        await store.dispatch("clientLogin", loginClient);
        //⑤store/presentationsでstateが更新されていることを確認
        expect(presentation_store.state.message).toBe(
          "IDもしくはパスワードに誤りがあります"
        );
      });
    });

解説

①使用するstoreをモジュール名でimport

indexストアはストア名を使い、presentationsストアは分かりやすい別名にしておきます

②storeのコピー

ストアの汚染を防ぐためにcloneDeepでstoreをコピーします。
ここでもstoreの変数名は"store"にして、presentationsストアのコピーは"presentation_store"としておきました。

参考: 実行可能なストアのテスト

③storeのモック

コピーした、presentationsストアをモックします。
ここでの変数名はindexストア内でdispatchされるモジュール名に合わせましょう。

ここからがポイントです

        showAndCloseMessage: ({}, arg) =>
          // actions内で他モジュールのactionをdispatchしている部分
          // "presentations/showAndCloseMessage"がdispatchされた時に内部でstore/presentations内のactionを呼ぶ
          presentation_store.dispatch("showAndCloseMessage", {
            message: arg.message,
            isSuccessMessage: arg.isSuccessMessage
          })

モック内のshowAndCloseMessage内では、ダミーのアクション(jest.fn())ではなく、先ほどimportしてきたstore内のshowAndCloseMessageを実際にdispatchします。

これにより、モックでactionが呼ばれたかどうかだけでなく、その後stateが正しく更新されたかどうかまで確認できるようになりました。

④モックをテスト対象のstoreに組み込む

これでpresentationストアのモックからactionの呼び出しができるようになりましたが、これがindex.jsのコピーであるstoreから呼べなければ意味がありません。

そこでstoreに、presentationsストアのモックをpresentationsモジュールとして登録します。
store.registerModuleメソッドを使えば、storeを作成した後にモジュールが登録できるのでこれを使います

⑤認証エラー時にpresetationsストアのstateが更新されていることを確認

    mock.onPost("/api/client/login").reply(401);

この部分で、axiosをモックして認証エラーを返すようにしています。
この状態でテストをすると、フラッシュメッセージが表示されるはずです。

        expect(presentation_store.state.message).toBe(
          "IDもしくはパスワードに誤りがあります"
        );

実際に、上記のテストでpresentationsストアのmessageの値を確認すると、正しいmessageに更新されていることが確認できました。
presentationsストアのstateは、コピーしてきたpresentation_storeのstateから確認できます。

長々と書きましたが、いろいろなドキュメントやサイトを見ながら書いたので、もっと良い書き方があればいつでもマサカリお待ちしています。

amakawa_
フロントエンドエンジニア。最近TS触り始めました。Vue.js/Nuxt.js/jest/Ruby on Rails
https://twitter.com/YuKi313230323?lang=ja
admin-guild
「Webサービスの運営に必要なあらゆる知見」を共有できる場として作られた、運営者のためのコミュニティです。
https://admin-guild.slack.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away