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


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

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

(Nuxt.jsの[モジュールモード](https://ja.nuxtjs.org/guide/vuex-store/)で書いた例です。Vuexのみの場合は書き方が違う可能性があります。)

例: store/index.js

```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);
          }
        }
      }
    };
```
```js
    { root: true }
```

この部分はグローバル[名前空間](https://vuex.vuejs.org/ja/guide/modules.html#%E5%90%8D%E5%89%8D%E7%A9%BA%E9%96%93)にアクションをコミットするために必要です。

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



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

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

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


store/index.js

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

```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"としておきました。

参考: [実行可能なストアのテスト](https://vue-test-utils.vuejs.org/ja/guides/using-with-vuex.html#%E5%AE%9F%E8%A1%8C%E5%8F%AF%E8%83%BD%E3%81%AA%E3%82%B9%E3%83%88%E3%82%A2%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88)

#### ③storeのモック

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

ここからがポイントです

```js

        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](https://vuex.vuejs.org/ja/guide/modules.html#%E5%8B%95%E7%9A%84%E3%81%AB%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B)メソッドを使えば、storeを作成した後にモジュールが登録できるのでこれを使います

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

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

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

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


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



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