fanfanta
@fanfanta (fan fanta)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

Vuex + vuex-module-decorators でのactionの単体テストの方法

解決したいこと

Vue.js + TypeScript + Vuexでプロジェクト作成しています。
Vuexをクラス形式で記述するためにvuex-module-decorators
を使用しています。

vuex-module-decoratorsを使用した際にVuexのstateをモック化し、actionの単体テストを行う方法について教えていただきたいです。

以下の環境で開発しています。

  • Vue.js: 2.6.19
  • Vuex: 3.0.1
  • vuex-module-decorators: 0.9.9
  • 単体テストユニット: Jest

作成したVuexモジュールと単体テストコード

単体テスト方法を検証するために以下のようにvuex-module-decoratorsを使いVuexモジュールを記述しています。

import { Action, Module, Mutation, VuexModule } from "vuex-module-decorators";

@Module({ name: "counter", namespaced: true })
export class Counter extends VuexModule {

  // state
  private count: number = 0;

  // mutaion
  // カウンターをインクリメントする
  @Mutation
  public increment(): void {
    this.count++;
  }

  // getter
  // カウンターの値を取得
  get getCount() {
    return this.count;
  }

  // action
  // カウンターの値を2インクリメントするアクション
  @Action({})
  public add2(): void {
    this.increment();
    this.increment();
  }
}

単体テストコードを以下のように記述しています。mutationのテストは想定通り作成することができましたが、actionのテストの記述方法が分からないという状態です。

actionの呼び出し部分で下記ソースコード中のコメントのようにエラーが発生します。

import Vuex from "vuex";
import { Counter } from "@/store/modules/counter";
import { createLocalVue } from "@vue/test-utils";

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

describe("Counter test", () => {

  // mutationのテストは想定通りできています
  it("mutation test increment1", () => {

    // stateのモック
    const mockState = {
      count: 0
    };

    Counter.mutations!.increment(mockState, {});
    expect(mockState.count).toBe(1);
  });

  // actionのテスト
  it("action test", () => {

    // stateのモック
    const mockState = {
      count: 3
    };

    // ここで以下のエラーが発生します。
    //  Cannot invoke an expression whose type lacks a call signature. Type 'Action<any, any>' has no compatible call signatures.
    Counter.actions!.add2(mockState);

    expect(mockState.count).toBe(5);
  });
});

解決方法や他に良いテスト方法などをご存知の方がいらっしゃいましたら、ご教示お願いいたします。

0

4Answer

vuexのことは何もわかりませんが、エラーからすると Counter.actions!.add2 の型は Action<any, any> ですね。型定義を覗いてみましたが

type Action<S, R> = ActionHandler<S, R> | ActionObject<S, R>;

となっています。 ActionHandlerActionObject の定義を辿ると

type ActionHandler<S, R> = (this: Store<R>, injectee: ActionContext<S, R>, payload?: any) => any;
interface ActionObject<S, R> {
  root?: boolean;
  handler: ActionHandler<S, R>;
}

ですので、 ActionHandler は関数呼び出し可能ですが ActionObject は関数呼び出し不可能です。
add2ActionHandler であることは型システムには分からないので、

(Counter.actions!.add2 as ActionHandler<any, any>)(mockState);

のように型強制するか、

if (typeof Counter.actions!.add2 === 'function') {
    Counter.actions!.add2(mockState)
}

などのように型ガードの中で記述すれば動くと思います。

1Like

@kazatsuyu さん、ご回答ありがとうございます。

ご回答頂いたようにactionの呼び出しを以下のように変更しました。
add2が関数呼び出しできるかの確認のため引数はいったんnullにしています。

(Counter.actions!.add2 as ActionHandler<any, any>)(null, null);

add2はActionHandlerとして関数呼び出しできましたが、

Counter.actions!.add2 に対して以下のようなエラーとなりました。

The 'this' context of type 'ActionTree' is not assignable to method's 'this' of type 'Store'.

ActionHandlerの第一引数のthisの型がStoreのため型が合わないようです。
度々で申し訳ありませんが、対処方法について何かご存知でしたらご教示お願い致します。

0Like

ああ、 this の型が指定されていましたね。
ソース断片を眺めただけの雑な理解なのでうまく動かなかったら申し訳ないですが、そもそも @Action デコレータの役割が this を置き換えるためのもので、 this に何を渡しても利用されることはないように見えるので、

(Counter.actions!.add2 as ActionHandler<any, any>).call(null as Store, null, null);

としてしまっても良いように見えます。

0Like

@kazatsuyu さん、ご回答ありがとうございます。

各引数はnullは指定できませんでしたが、以下のmockStoreのようにモック化していけばいけると思います。
ご丁寧にご回答頂きありがとうございました。勉強になりました。

    const mockState = {
      count: 3
    };

    const mockStore = new Vuex.Store({
      state: mockState
    });

    (Counter.actions!.add2 as ActionHandler<any, any>).call(
      mockStore,
      null as ActionContext<any, any>
      null
    );
0Like

Your answer might help someone💌