阅读 154

手把手让你像使用vuex一样测试vuex

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进行测试。

关注下面的标签,发现更多相似文章
评论