手把手讓你像使用vuex一樣測試vuex

談笑斗酒發表於2019-07-21

vuex怎麼單元測試,我們只能通過檢驗state的值是否符合預期來測試,所以,正常的套路應該是測試mutation,然後看看對應的state是否發生了符合預期的變化。沒錯。原文地址vue單元測試vuex,mutation,尤其是actions、getters怎麼測?讓你像使用vuex一樣測試vuex

mutation 怎麼測

比如這種

SET_LIST(state, payload) {
      state.listData = payload;
},
複製程式碼
// test.spec.js
describe("mutations", async () => {
  it("mutations SET_LIST", async function() {
    SET_LIST(state, [123]);
    expect(state.listData).eql([123]);
  });
}
複製程式碼

每個mutation就是一個方法,我們直接呼叫 SET_LIST(state, payload)檢驗對應的state就可以完成對SET_LIST的測試了。這當然簡單了,畢竟mutation裡面就這兩個引數

action呢

action裡面一般伴有傳送請求,然後根據請求結果,觸發對應的mutation,有時我們還會在action裡面觸發另外的action等較複雜的情況 下面列舉了我們不願意測action的原因:

  1. action可能進行介面呼叫
  2. action可能傳送當前module或其他module的state或getters
  3. action可能傳送當前module或其他module的action
  4. 上述情況可能會組合出現

在測試這樣的action時是不是會吐槽,action怎麼會寫這麼複雜。哈哈,業務需要啊。 action裡面也是兩個引數,只是第一個引數context是一個物件

file
除了staterootStategettersrootGetters這幾個物件用來獲取值以外,裡面還有commitdispatch方法。 比如下面的兩個action就比較複雜,還有“巢狀”關係

    async _getDetail({ rootState, dispatch }, params) {
      const [res] = await Promise.all([
        rootState.Axios.post(rootState.Api.pim.propGetAttr, params),
        rootState.pim.root.langResult
      ]);
      dispatch("handleDetailResponse", { res, params });
      return res;
    },
    // 處理新建/編輯屬性的通用response
    async handleDetailResponse({ commit, dispatch }, { res, params }) {
      if (httpSuccess(res)) {
        commit("SET_DETAIL", res.data.data.data);
        const paginator = (res.data.data && res.data.data.paginator) || {};
        if (paginator.totalCount > paginator.limit) {
          dispatch("_getDetailAllValueVOList", {
            ...params,
            page: {
              page: 1,
              size: paginator.totalCount
            }
          });
        } else {
          const { valueVOList } = res.data.data.data || {};
          commit("SET_ATTR_VALUES", valueVOList);
          commit("SET_ATTR_VALUE_PAGINATOR", paginator);
        }
      }
    },
複製程式碼
問題一:這種我們怎麼測試_getDetail這個action呢?跟mutation一樣?不行啊,第一個引數{commit, dispatch}commitdispatch怎麼傳?
問題二:如果在store裡面使用this怎麼辦?this.dispatch或者this.state.pim.listData這種寫法怎麼辦?

action裡面的this是含有下列屬性的。

file

市面上的解決方案

測試action的童鞋自然是發現這個問題的,所以,大家一般都選擇了“曲線救國”--拆分action測試

  1. 只測試對應的action,只要傳送了就行
  2. action裡面的mutation模擬資料後測試
  3. 裡面巢狀其它action、mutaion一樣也進行拆分

file

上圖所示的Action1的測試會分成2個部分進行測試。

  1. Action1 -> Action2
  2. Mutation3
  3. Mutation1
  4. Mutation2

其實Action1 -> Action2好不好測試,我這裡存疑。 好多人都是用sinon來實現測試action,sinon怎麼用我不知道,我只是通過程式碼來看,這種測試action的沒什麼用,就是為了提交單元測試覆蓋率而已。

file
file

我怎麼測?

直接複用store裡面的所有程式碼來實現全流程測試(介面資料肯定得模擬了) 這樣的話,擋在前面的問題就出現了,在store的方法裡面的commitdispatchthis怎麼辦? 我們不是學過使用bindcall嗎?難道這些只是為了面試的嗎,遇到問題解決問題啊。 store的每個module就是一個物件啊,裡面有屬性statemutationsactionsgetters而已。按照vuex的使用方式模擬一個基本跟原有commitdispatch一樣功能的函式即可。 先看效果。 原有store如下,有多層子module:

// store.js
export default {
  namespace: true,
  state: {
    abc: 1
  },
  mutations: {
    SET(state, payload) {
      state.abc = payload;
    },
    PLUS(state, payload) {
      state.abc = state.abc + payload;
    },
  },
  actions: {
    _plus(context, params) {
      console.log("context -> ", context);
      context.commit("PLUS", params);
    }
  },
  getters: {
    getStatePlus(state, getters, rootState, rootGetters) {
      console.log(
        "getters getStatePlus-> ",
        state,
        rootState,
        Object.getOwnPropertyNames(getters),
        Object.getOwnPropertyNames(rootGetters)
      );
      return state.abc + 1;
    }
  },
  modules: {
    sub: {
      namespace: true,
      state: {
        cbd: 100
      },
      mutations: {
        SET(state, payload) {
          state.cbd = payload;
        },
        MINUS(state, payload) {
          console.log(" MINUS arg-> ", arguments);
          state.cbd = state.cbd - payload;
          console.log(" MINUS result-> ", state.cbd);
        }
      },
      actions: {
        _minus(context, params) {
          console.log("context -> ", context);
          context.commit("MINUS", params);
        }
      },
      getters: {
        getStateMinus(state, getters, rootState, rootGetters) {
          console.log(
            "getters getStateMinus-> ",
            state,
            rootState,
            Object.getOwnPropertyNames(getters),
            Object.getOwnPropertyNames(rootGetters)
          );
          return state.cbd - 1;
        }
      },
      modules: {
        subTie: {
          namespace: true,
          state: {
            xyz: 55
          },
          mutations: {
            SET(state, payload) {
              state.xyz = payload;
            },
            MULTIPLY(state, payload) {
              state.xyz = state.xyz * payload;
            }
          },
          actions: {
            _multiply(context, params) {
              console.log("context -> ", context);
              context.commit("MULTIPLY", params);
            }
          },
          getters: {
            getStateMultiplyDouble(state) {
              console.log("getters getStateMultiplyDouble-> ", arguments);
              return state.xyz * 2;
            }
          }
        }
      }
    }
  }
};
複製程式碼

在測試用例裡面使用也很簡單,跟我們在action裡面觸發mutation和action一致,是不是很爽。

// test.spec.js
import { expect } from "chai";
import VuexTester from 'vuex-tester';
import store from '../store';
const {commit, dispatch, rootState, state, getters, rootGetters} = new VuexTester(store).update();

describe("test state", async () => {
  it("state abc", async function() {
    expect(state.abc).eql(1);
  });
});
describe("test rootState", async () => {
  it("rootState abc", async function() {
    expect(rootState.abc).eql(1);
  });
});
describe("test mutations", async () => {
  it("mutations SET", async function() {
    commit('SET', 100);
    expect(state.abc).eql(100);
  });
  it("mutations PLUS", async function() {
    commit('SET', 100);
    commit('PLUS', 10);
    expect(state.abc).eql(110);
  });
});
describe("test actions", async () => {
  it("actions _plus", async function() {
    commit('SET', 100);
    dispatch('_plus', 9);
    expect(state.abc).eql(109);
  });
});
describe("test getters", async () => {
  it("getters getStatePlus", async function() {
    commit('SET', 100);
    expect(getters.getStatePlus).eql(101);
  });
});
describe("test rootGetters", async () => {
  it("rootGetters getStatePlus", async function() {
    commit('SET', 100);
    expect(rootGetters.getStatePlus).eql(101);
  });
});
複製程式碼

我放部分核心程式碼

update(storeContext = this.store, namespace = this.namespace) {
    const fn = this.getFn(namespace);
    const {
      state = {},
      actions = {},
      mutations = {},
      getters = {}
    } = storeContext;
    const boundCommit = (type, payload) => {
      console.log("[commit  ]: ", type, payload);
      // mutation in vuex return noting, but we return state
      return (
        fn(type, this.rootMutationsMap, mutations).call(
          this.storeContext,
          state,
          payload
        ) || state
      );
    };

    const boundDispatch = (type, payload) => {
      console.log("[dispatch]: ", type, payload);
      return fn(type, this.rootActionsMap, actions).call(
        this.storeContext,
        this.context,
        payload
      );
    };
    this.storeContext.commit = boundCommit;
    this.storeContext.dispatch = boundDispatch;

    this.initGetter(namespace, getters, state);
    // core state and function in vuex context
    this.context = {
      rootState: this.storeContext.state,
      state: state,
      commit: boundCommit,
      dispatch: boundDispatch,
      getters: this.gettersMap,
      rootGetters: this.rootGettersMap
    };
    return this.context;
  }
複製程式碼

哈哈,看到你心心念唸的commitdispatchstategettersthis了吧 我把自己的想法發到了npm上面,地址vuex-tester。 具體程式碼在github上面。 建議clone下來,直接執行npm run test進行測試。

相關文章