Search code examples
typescriptunit-testingvue.jsmocha.jsvuex

What is the correct way for Mock my ActionContext in my unit test case?


I have trouble for Mocking my ActionContext in my unit test. Actually, I have Mock my ActionContext in this way :

testUtilities.ts

import { ActionContext, Dispatch, Commit } from 'vuex';
import { IRootState } from './../src/store/store';
import { expect } from 'chai';
import { DispatchAccessorNoPayload } from 'vuex-typescript';

export interface ContextTest {
    state: any;
    rootState: IRootState;
    getters: any;
    rootGetters: any;
}

type TestActionTypes = (action: DispatchAccessorNoPayload<any, IRootState, any>,
    {
        state,
        rootState,
        getters,
        rootGetters,
    }: ContextTest,
    expectedMutations: any,
    done: Mocha.Done) => void;

export const testAction: TestActionTypes = (
    action,
    {
        state,
        rootState,
        getters,
        rootGetters,
    },
    expectedMutations,
    done
) => {
    let countDispatch = 0;
    let countCommit = 0;

    // mock dispatch
    const dispatch: Dispatch = (type: string) => {
        return new Promise((resolve, reject) => {
            const mutation = expectedMutations[countDispatch];
            try {
                expect(mutation.type).to.equal(type);
            } catch (error) {
                done(error);
                reject(error);
            }

            countDispatch++;

            if (countDispatch >= expectedMutations.length) {
                done();
                resolve();
            }
        });
    };

    // mock commit
    const commit: Commit = (type: string) => {
        const mutation = expectedMutations[countCommit];
        try {
            expect(mutation.type).to.equal(type);
        } catch (error) {
            done(error);
        }
        countCommit++;
        if (countCommit >= expectedMutations.length) {
            done();
        }
    };

    const context: ActionContext<any, IRootState> = {
        state,
        rootState,
        rootGetters,
        commit,
        dispatch,
        getters,
    };

    // call the action with mocked store and arguments
    action(context);

    // check if no mutations should have been dispatched
    if (expectedMutations.length === 0) {
        expect(countCommit).to.equal(0);
        expect(countDispatch).to.equal(0);
        done();
    }
};

I use this helper from the documentation of Vuex : https://vuex.vuejs.org/guide/testing.html#testing-actions

customer.module.ts

import { IRootState } from "./../store";
import { ICustomer } from "./../../models/customer.model";
import { ActionContext } from "vuex";
import { getStoreAccessors } from "vuex-typescript";
import { CustomerService } from './../../services/entities/customer.service';

export interface ICustomersState {
  customers: ICustomer[];
}

const initialState: ICustomersState = {
  customers: []
};

type CustomersContext = ActionContext<ICustomersState, IRootState>;

export const mutations = {
  FETCH_CUSTOMERS(state: ICustomersState, customers: ICustomer[]) {
    state.customers = customers;
  }
};

export const actions = {
  GETALL_CUSTOMERS(context: CustomersContext) {
    let customerService = new CustomerService();
    return customerService.getAll().then(customers => {
      context.commit("FETCH_CUSTOMERS", customers);
    });
  },
}

export const customer = {
  namespaced: true,
  state: { ...initialState },
  mutations,
  actions
};

const { read, commit, dispatch } = getStoreAccessors<ICustomersState, IRootState>("customers");

export const getAllCustomersDispatch = dispatch(customer.actions.GETALL_CUSTOMERS);

customer.actions.spec.ts

import { getAllCustomersDispatch } from './../../../../src/store/modules/customer';
import { testAction, ContextTest } from "./../../../../tests/testUtilities";

describe("Customer Actions", () => {
    it("GETALL_CUSTOMERS", (done) => {
        const context: ContextTest = {
            state: [],
            getters: {},
            rootGetters: {},
            rootState: {
                customers: {
                    customers: []
                }
            }
        }
        testAction(getAllCustomersDispatch, context, [{
            type: 'customers/FETCH_CUSTOMERS'
        }], done)
    });
});

Expected Result

I would like my unit test verify if the action GETALL_CUSTOMERS well call my commit FETCH_CUSTOMERS.

Test OUTPUT

+ expected - actual

-"customers/FETCH_CUSTOMERS"
+"customers/GETALL_CUSTOMERS"

I put a breakpoint inside commit constant in my testAction function, but my treatment doesn't go through this one.


Solution

  • I solved my problem, I used sinon.js for mock my API call and mock my commit.

    I get the expected result well.

    customer.actions.spec.ts

    import { actions } from "./../../../../src/store/modules/customer";
    import { spy, createSandbox } from "sinon";
    import { expect } from "chai";
    import Axios from "axios";
    import Sinon = require("sinon");
    import { ICustomer } from "src/models/customer.model";
    
    describe("Customer Actions", () => {
      let sandbox: Sinon.SinonSandbox;
      beforeEach(() => (sandbox = createSandbox()));
    
      afterEach(() => sandbox.restore());
    
      it("GETALL_CUSTOMERS", async () => {
        // Assign
        let data: ICustomer[] = [
          {
            id: 1,
            address: "",
            company: "",
            firstName: "",
            lastName: "",
            zipcode: "",
            siret: "",
            tel: "",
            projects: []
          }
        ];
        const resolved = new Promise<any>(r => r({ data }));
        sandbox.stub(Axios, "get").returns(resolved);
    
        let commit = spy();
        let state = {
          customers: []
        };
        const getters: {} = {};
        let rootGetters: {} = {};
        let rootState: {
          customers: {
            customers: [];
          };
        } = {
          customers: {
            customers: []
          }
        };
        // Act
        await actions.GETALL_CUSTOMERS({
          commit,
          state,
          dispatch: () => Promise.resolve(),
          getters,
          rootGetters,
          rootState
        });
        // Assert
        expect(commit.args).to.deep.equal([
          ["FETCH_CUSTOMERS_REQUEST"],
          ["FETCH_CUSTOMERS_SUCCESS", data]
        ]);
    })
    

    customer.module.ts

    import { IRootState } from "./../store";
    import { ICustomer } from "./../../models/customer.model";
    import { ActionContext } from "vuex";
    import { getStoreAccessors } from "vuex-typescript";
    import { CustomerService } from './../../services/entities/customer.service';
    
    export interface ICustomersState {
      customers: ICustomer[];
    }
    
    const initialState: ICustomersState = {
      customers: []
    };
    
    type CustomersContext = ActionContext<ICustomersState, IRootState>;
    
    export const mutations = {
      FETCH_CUSTOMERS(state: ICustomersState, customers: ICustomer[]) {
        state.customers = customers;
      }
    };
    
    export const actions = {
      async GETALL_CUSTOMERS(context: CustomersContext) {
        let customerService = new CustomerService();
        context.commit("FETCH_CUSTOMERS_REQUEST", customers);
        await customerService.getAll().then(customers => {
          context.commit("FETCH_CUSTOMERS_SUCCESS", customers);
        }).catch(err => context.commit("FETCH_CUSTOMERS_ERROR", err));
      },
    }
    
    export const customer = {
      namespaced: true,
      state: { ...initialState },
      mutations,
      actions
    };
    
    const { read, commit, dispatch } = getStoreAccessors<ICustomersState, IRootState>("customers");
    
    export const getAllCustomersDispatch = dispatch(customer.actions.GETALL_CUSTOMERS);
    

    I deleted the file testUtilities.ts because I didn't need it anymore.