Unit Testing of Vuex Actions with Mocha and Sinon

Unit testing your Vuex-powered state management includes verifying getters, mutations, and actions. Because of the synchronous nature of getters and mutations, testing them is straight forward. On the other hand, testing actions can be sometimes tricky since most of the time asynchronous code is involved. Especially testing the key functionality of your actions is not easy – are your actions dispatching mutations correctly in the right context with the right arguments?

This article is only concerned with testing actions. Because it took me quite some time to figure out how I can establish a testing strategy that allows for code coverage of 100%, I hope my article offers you some helpful information on this topic.

This article is structured into the following parts:

  • Strategy for Testing Vuex Actions – This constitutes the most important part of this article. Here, I try to provide code examples and explain them in great detail. The goal here is that you get a better understanding of Vuex and how to test things. How to mock Vuex’s commit function is in the focus of this section.
  • Overview of Testing Use Cases – This section takes up the learnings from the previous section and provides a quick overview on different testing approaches, especially on how to isolate 3rd-party dependencies, such as remote calls. To get a quick overview on how my testing approach looks like, just jump directly to this section.

Strategy for Testing Vuex Actions

First, I explain my testing concept, which libraries I use, as well as the testing skeleton. After that, I go into details on how to mock Vuex’s commit function and how to cope with asynchronous code, e.g., REST calls.

Project Setup

The easiest way to have a Vue.js project setup with Mocha and Sinon in place is to use the official Vue.js Webpack Template. Start the wizard with the following shell command:

$ vue init webpack my-project
  • Select "Set up unit tests"
  • Select "Karma and Mocha"

This installs all required npm modules for you.

Recently, using [Jest] with Vue has came up. At the end of this article, I reveal my thoughts on this topic.

Skeleton for Testing Actions and Involved Technologies

The next code snippet represents our skeleton for testing Vuex actions.

// actions.spec.js

import sinon, { expect } from "sinon";
import sinonChai from "sinon-chai";
import chai from "chai";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
let mock = new MockAdapter(axios);

import { testAction } from "./testUtils";

import actions from "@/store/actions";
import * as mutationTypes from "@/store/mutation-types";

chai.use(sinonChai);

describe("actions", () => {
  beforeEach(function() {
    mock.reset();
  });

  it("should process payload and commits mutation for successful GET", done => {
    /* testing code here */
  });
});

Let’s go through the import statements to see which technologies are involved. Sinon.js (short Sinon) provides standalone test spies, stubs, and mocks that can also be used with Mocha. Mocha represents the testing framework. I also utilize Chai that constitutes a BDD assertion library. Sinon-Chai allows for writing nice Chai assertions together with mocking capabilities provided by Sinon. The following line extends Chai with further assertions for Sinon.

chai.use(sinonChai);

Besides Sinon, there are alternative ways for mocking dependencies. I also use Axios Mock Adapter to isolate the actual remote calls. In addition, it offers a powerful feature to test actions with different remote call responses. E.g., you can verify that a remote call was successful.

Later, I show you testing examples using AxiosMock. In order to ensure that every test is independent from each other, created mocks need to be destroyed after every test. The following beforeEach function does exactly this:

beforeEach(function() {
  mock.reset();
});

With the help of the following import statement, I bring in my helper function to verify that Vuex actions dispatch mutations correctly.

import { testAction } from "./testUtils";

In the next section, I walk through this helper function in great detail since it is the crucial part of my testing strategy.

How to Implement Action Tests?

Before I answer this question, let’s have a quick recap of a Vuex action.

export const loadNextBreakfast = function({ commit }) {
  axios
    .get("/api/getnextbreakfast")
    .then(response => {
      commit(mutationTypes.SET_NEXT_BREAKFAST, response.data);
      commit(mutationTypes.CHANGE_AVAILABLE_FOODS, [...response.data.foodlist]);
    })
    .catch(e => {
      commit(mutationTypes.FAILURE, "500");
    });
};

Besides the fact, that virtually every time a remote call is performed (we deal with this fact later), an action will commit one or more mutations based on the context. A context arises, e.g., by a remote call response or by providing particular function arguments, e.g., a state or an action payload.

The main purpose of actions is to dispatch or commit mutations, respectively. Thus, most of the time I want to verify that Vuex’s commit function is invoked correctly with the right arguments. Since I do not want to invoke the actual mutation implementation, I have to mock the commit function. In the next section we deal with this aspect.

Mocking Vuex’s Commit Function

Based on the official Vuex documentation for testing actions, I created a file named testUtils.js as testing helper. In principle, Vuex’s documentation provides all necessary information. However, I had a hard time to understand the mechanism due to the fact that it is spare on the topic of testing actions.

I utilize testAction function in tests in order to verify that the action under test invokes the correct mutations with right arguments and in right order.

Before I go into implementation details of the helper function, I show you how I want to use it later in tests.

it("should invoke correct mutations for successful GET", done => {
  const response = {
    foodlist: ["salmon", "peanut butter"]
  };
  mock.onGet("/api/getnextbreakfast").reply(200, response);
  const actionPayload = null;
  const state = null;
  const expectedMutations = [
    {
      type: mutationTypes.SET_NEXT_BREAKFAST,
      payload: response
    },
    {
      type: mutationTypes.CHANGE_AVAILABLE_FOODS,
      payload: response.foodlist
    }
  ];
  testAction(loadNextBreakfast, actionPayload, state, expectedMutations, done);
});

The example above verifies that the Vuex action "loadNextBreakfast" commits the expected mutations, described with the array expectedMutations. type and payload represent the arguments of Vuex’s commit function. I also want to verify inside the helper function that the correct mutations were triggered with the correct arguments. payload can be null, if a mutation needs to be committed with a type only. actionPayload and state are optional arguments and, thus, can be null. If the action under test needs to operate on state and / or requires a payload object as input, you have to provide it with these two arguments.

testAction contains the logic to mock the commit function.

// testUtils.js
import { expect } from "chai";

export const testAction = (
  action,
  actionPayload,
  state,
  expectedMutations,
  done
) => {
  let count = 0;
  let commit = (type, payload) => {
    let mutation = expectedMutations[count];
    try {
      // check if commit function is invoked with expected args
      expect(mutation.type).to.equal(type);
      if (payload) {
        expect(mutation.payload).to.deep.equal(payload);
      }
      count++;
      // check if all mutations have been dispatched
      if (count >= expectedMutations.length) {
        done();
      }
    } catch (error) {
      done(error);
    }
  };

  if (expectedMutations.length === 0) {
    expect(count).to.equal(0);
    done();
  } else {
    action({ commit, state }, actionPayload);
  }
};

I think it might help to break the code into pieces and explain it step by step. Let’s start with the function arguments.

export const testAction = (
  action,
  actionPayload,
  state,
  expectedMutations,
  done
) => {
  // ...
};
  • action refers to the function name of the action that needs to be called in the context of the test. For the example above, the action is "loadNextBreakfast".
  • actionPayload constitutes the payload that is passed as argument to the action under test. We pass null if the action does not process a payload.
  • state represents the Vuex state object that is required by the action under test. The object must just define the properties needed by the action. It can also be null if the action does not operate on the state at all.
  • expectedMutations is an array containing one or more objects, which hold the arguments for invoking the commit function. Here is a recap of what it looks like:
const expectedMutations = [
  {
    type: mutationTypes.SET_NEXT_BREAKFAST,
    payload: response
  },
  {
    type: mutationTypes.CHANGE_AVAILABLE_FOODS,
    payload: response.foodlist
  }
];

Next up, the function body:

let count = 0;
let commit = (type, payload) => {
  /* ... */
  count++;
  /* ... */
};
if (expectedMutations.length === 0) {
  expect(count).to.equal(0);
  done();
} else {
  action({ commit, state }, actionPayload);
}

testAction declares a count variable initialized to 0 outside of the mock commit function. count gets increased whenever commit is invoked by the action that gets this mock commit passed as argument.

The whole process is triggered by calling the actual action inside the else clause. action refers to the Vuex action under test that is passed as argument to testAction. Besides our mock commit function, I also pass a mock state and an actionPayload as arguments to testAction. The if clause handles the case that an action does not commit any mutation. done() is important to signal Mocha that the asynchronous test code is completed.

Finally, I take a closer look at the arrow function that constitutes the mock commit function.

let commit = (type, payload) => {
  let mutation = expectedMutations[count];
  try {
    expect(mutation.type).to.equal(type);
    if (payload) {
      expect(mutation.payload).to.deep.equal(payload);
    }
    count++;
    if (count >= expectedMutations.length) {
      done();
    }
  } catch (error) {
    done(error);
  }
};

First, I extract the mutation with the current count from the passed expectedMutations array. Remember, such a mutation object looks like this:

  {
    type: mutationTypes.SET_NEXT_BREAKFAST,
    payload: response
  }

Then, I verify that type and payload do match. Since not every mutation needs a payload, I verify that the payload object is defined. It is important to call done() if all expected mutations have been dispatched, otherwise I get a Mocha error. Additionally, I use a try-catch block for the same reason in order to terminate the unit test in case of an exception.

In summary, the test above verifies that "loadNextBreakfast" action dispatches two mutations with right type and payload in correct order.

Testing Asynchronous Code

In principle, testing Vuex actions also means to isolate asynchronous code in order to make unit tests independent from external code (e.g., 3rd-party dependencies or backend code). In the example above, "loadNextBreakfast" uses Axios to perform REST calls.

Thus, the task is to mock axios.get().

export const loadNextBreakfast = function({ commit }) {
  axios
    .get("/api/getnextbreakfast")
    .then(response => {
      /* ... */
    })
    .catch(e => {
      /* ... */
    });
};

In the next section, I present two approaches to prevent the invocation of the actual implementation of axios.get(). First, testing use case 2 leverages AxiosMock to circumvent the original axios call. Testing use case 3 demonstrates how Sinon can be used to have external dependencies under control.

Overview of Testing Use Cases

Testing Use Case 1: Verifying that Actions Dispatch Mutations correctly

As described in great detail in the previous section, mocking commit is crucial for testing Vuex actions.

it("should set participation to value attending", done => {
  const payload = {
    availability: true,
    user: {
      username: "doppelmutzi",
      name: "Sebastian Weber",
      avatar: "url"
    }
  };
  const state = {
    breakfast: {
      participantIds: []
    }
  };
  const expectedMutations = [
    {
      type: mutationTypes.SET_PARTICIPATION,
      payload: {
        user: payload.user,
        availability: mutationTypes.AVAILABILITY_ATTENDING
      }
    }
  ];
  testAction(toggleAvailability, payload, state, expectedMutations, done);
});

Such a test sets up testAction’s arguments first and then calls it. Then it checks whether the expectations (defined by expectedMutations) are fulfilled. For a detailled description on how testAction works, go to the section about mocking Vuex’s commit function.

Testing Use Case 2: Using Axios Mock Adapter for Testing Successful and Failing Requests

Remember, I created a mock property in the testing skeleton:

/* ... */
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
let mock = new MockAdapter(axios);
/* ... */

describe("actions", () => {
  beforeEach(function() {
    mock.reset();
  });
  /* ... */

I utilize mock to check whether the action under test operates correctly for successful and failing axios calls. In the following snippet, the first test is an example for a remote call responding with status code of 200. The second test checks that the correct mutation is dispatched in case of a network error.

it("should process payload and invoke correct mutation
  for successful GET", done => {
  const response = {
    foodlist: ["salmon", "peanut butter"]
  };
  mock.onGet("/api/getnextbreakfast").reply(200, response);
  const payload = null;
  const state = null;
  const expectedMutations = [
    {
      type: mutationTypes.SET_NEXT_BREAKFAST,
      payload: response
    },
    {
      type: mutationTypes.CHANGE_AVAILABLE_FOODS,
      payload: response.foodlist
    }
  ];
  testAction(loadNextBreakfast, payload, state, expectedMutations, done);
});

it("should invoke failure mutation for network error", done => {
  mock.onGet("/api/allusers").networkError();
  const payload = null;
  const state = null;
  const expectedMutations = [
    {
      type: mutationTypes.FAILURE,
      payload: "500"
    }
  ];
  testAction(loadNextBreakfast, payload, state, expectedMutations, done);
});

Axios Mock Adapter offers a nice API to easily mock server responses, such as status codes or network errors. It is easy to use. Just setup the desired server response before you actually call the action under test. Then, the axios call inside of the action implementation is replaced with the mock version. However, if you just want basic mocking of axios, you can also use Sinon as described next.

Testing Use Case 3: Mocking 3rd-party Libraries with Sinon.js

This technique can also be used to mock any external code, not only axios calls. Take a look at the following action "toggleAvailability" that follows an "optimistic update" approach – i.e., trigger a mutation immediately without waiting for a remote call to return. Perform a rollback in case of a remote call error with the help of a "compensating commit" (mutationTypes.FAILED_SET_PARTICIPATION).

export const toggleAvailability = function({ commit, state }, payload) {
  const savedParticipantIds = [...state.breakfast.participantIds];
  const availability =
    payload.availability === true
      ? mutationTypes.AVAILABILITY_ATTENDING
      : mutationTypes.AVAILABILITY_ABSENT;
  const { user } = payload;
  commit(mutationTypes.SET_PARTICIPATION, {
    user,
    availability
  });
  if (!payload.stomp) {
    axios
      .put(`/api/setParticipation/${user.username}/${availability}`)
      .then(response => {})
      .catch(e => {
        commit(mutationTypes.FAILED_SET_PARTICIPATION, savedParticipantIds);
      });
  } else {
    spawnNotification(
      `${user.name} has ${
        availability === mutationTypes.AVAILABILITY_ATTENDING
          ? "signed in"
          : "signed out"
      }.`,
      user.avatar,
      "availability changed"
    );
  }
};

This example action has two contexts:

  1. The user triggers an event from the UI and the action invokes an axios call. This is indicated by a stomp flag set to false.
  2. The backend sends a Stomp event to the client and the action needs to dispatch a mutation without performing an axios call. This is indicated by a stomp flag set to true. In this context, a browser notification is spawned by invoking the spawnNotification function.

For the purpose of this article, it is not necessary to understand the whole implementation of this example action. Futhermore, it is not required to either know the Stomp protocol nor have knowledge how browser notifications work. No implementation details are relevant for the sake of this article and, thus, skipped. The provided action is a good example of how to isolate such external dependencies and instead concentrate on the actual code that is in scope of the action.

All in all, the test implementation has to mock external dependencies. Concrete, it has to cope with the axios remote call, the Vuex commit function as well as the spawnNotification function. The following code snippet shows a test implementation that makes use of Sinon to isolate all these dependencies.

it("should spawn a notification and should not perform an ajax call
  with stomp flag set to true", () => {
  const notificationStub = sinon.stub(notification, "spawnNotification");
  const axiosStub = sinon.stub(axios, "put");
  const commit = sinon.stub();
  const payload = {
    stomp: true,
    availability: true,
    user: {
      username: "doppelmutzi",
      name: "Sebastian Weber",
      avatar: "url"
    }
  };
  const state = {
    breakfast: {
      participantIds: []
    }
  };
  const expectedNotification = {
    body: payload.user.name + " has signed in",
    icon: payload.user.avatar,
    title: "availability changed"
  };

  toggleAvailability({ commit, state }, payload);

  sinon.assert.calledWith(
    notificationStub,
    expectedNotification.body,
    expectedNotification.icon,
    expectedNotification.title
  );
  expect(axiosStub).to.not.have.been.called;

  notificationStub.restore();
  axiosStub.restore();
});

After setting up stubs, state, and payload, this test verifies that the action named "toggleAvailability" spawns a notification but does not perform a remote call. With this kind of unit test I’m not interested in verifying that correct mutations are dispatched (see testing use case 1). Therefore, I also use Sinon to mock commit and just pass it as argument to the action function.

Jest – a Mocha Alternative

End of 2017, Vue.js’s official Webpack Template added support for Facebook’s Jest. The installation wizard allows for choosing between Mocha and Jest.

I leave undecided which technology is better because I haven’t looked into Jest yet. However, I see a lot of articles and tweets about people moving from Mocha to Jest.

Jest is especially popular for its snapshot testing. It also comes with mocking capabilities. However, most of the time, I have come across articles about Jest and how to use it for component testing. At least Lachlan Miller wrote about using Jest for testing Vuex actions.

It seems that one of Jest’s benefits is its performance. Edd Yerburg, one of the authors of vue-test-utils, gave a talk about testing Vue components and provided a comparison of Vue SFC unit tests using different test runners.

Up to now, it is not clear for me, if there are any obvious advantages of Jest over Mocha in terms of unit testing Vuex code.

Conclusion

Leaving the discussion out, which testing technology performs better, it is important to test your Vuex actions. With the testing approaches above (using Mocha), it is possible to have a 100% code coverage of Vuex actions as depicted in the next screenshot. I think you can also replace Mocha with Jest very easily and get the same test coverage.

Maybe you have the same attitude like me that actions are one of the most important but also most complex parts of your Vue.js application. That’s why testing them entirely is crucial!

With the testing approach 100% LOC of Vuex actions can be tested

Another good thing, if you establish such a testing approach, is that you also gain a better understanding of Vuex. Thereby, you become a better Vue developer.

It would be nice if you let me know what you think about my testing approach in the comments. How do you test your Vuex code?

Written on February 18, 2018