Testing my Vuex store

How do I want to test it

There are some great tutorials to test mutations and actions on the vuex page. It involves testing them separately, and involve more setup than I like. I'd rather have as many real collaborators as I can afford.

I want to import my store, verify the initial state. Send an action, and run assertions on the state after that. I am not really interested if the action dispatch two mutations or just one, if the logic is in actions or in mutations. I want to test the store as a Vue component would use it.

First line of code, first issue

// test/js/store.spec.js

import store from '../../resources/assets/js/store';

First problem, I import my store, and my store import actions, and my actions import a Http object, and this Http object import Vue Resource. Vue-resource try to assign something to the window object, and things blow up as I am running my test in a node environment with mocha.

I knew that I would have to mock at least my Http module, but I didn't think it would come this fast...

I need to mock an ES6 module imported by one of my collaborator. Fine. How do you do that?

Two solutions:

  • Mocking my Http object, something like this article from @grahamcox82 recommends.
  • Or, thanks to vuejs, alias my http module in my webpack test config.

I know that I will have to test Vue components later, so webpack seems to be the way to go.

Setup Karma with Webpack and Mocha. Can't be that hard?

I was running my tests directly in Mocha, so I need a little bit of setup for karma. Thanks to Evan You, I could browse some example setups on Github, rely on good documentation and have answers to my questions on twitter! What more do you want? Thanks for all this amazing work Evan!

The karma config result:

// build/karma.conf.js

var webpackConf = require('./webpack.base.conf');
delete webpackConf.entry;

module.exports = function(config) {
    config.set({
        browsers: ['PhantomJS'],
        singleRun: true,
        frameworks: ['mocha', 'chai'],
        files: [
            '../test/js/index.js',
        ],
        plugins: [
            'karma-phantomjs-launcher',
            'karma-chai',
            'karma-mocha',
            'karma-webpack',
            'karma-mocha-reporter',
        ],
        preprocessors: {
            '../test/js/index.js': ['webpack'],
        },
        reporters: ['mocha'],
        webpack: webpackConf,
        webpackMiddleware: {
            noInfo: true,
        },
    });
};

Test suite entry point

My test suite need an entry point to autoload every .spec.js file (this was also robbed from one of vue's repo):

// test/js/index.js

// Polyfill fn.bind() for PhantomJS
/* eslint-disable no-extend-native */
Function.prototype.bind = require('function-bind');

var testsContext = require.context('.', true, /\.spec$/);

testsContext.keys().forEach(testsContext);
module.exports = testsContext;

If like me you have problem with phantomjs not running, I had to make sure that PHANTOM_JS_BIN was declared properly in my .zprofile (or anywhere else).

I will spare you the webpack config as there is not much at the moment. This will get more interesting when I will tackle the mocking of asynchronous calls. For the moment, I just want to be able to run my test suite and get started.

I can run my test suite with karma start build/karma.conf.js. Sweet.

Testing my setup

This is my first testing experience with javascript, so I start with a couple tests to make sure everything can be loaded perfectly.

// test/js/store.spec.js

import { should } from 'chai';
import store from '../../resources/assets/js/store';
should();

describe('store test suite', () => {
    it('is configured correctly', () => {
        expect(true).to.equal(true);
    });
});

This works.

I also want to make sure that every test runs on the same initial state of my store. This is still specific to my store test suite, so I don't mind it being in the same file for now.

// test/js/store.spec.js

describe('store test suite', () => {
    it('is configured correctly', () => {
        expect(true).to.equal(true);
    });
    it('has a valid store object', () => {
        expect(store.state.dummy).to.be.null;
        store.actions.setDummy('test');
        store.state.dummy.should.equal('test');
    });
    it('runs tests on a fresh store instance', () => {
        expect(store.state.dummy).to.equal('test');
    });
});

Now it's broken. I need to refresh my store after each test. I need to store the initialState, and then apply it to my store after each test has run. Vuex doesn't offer an API (yet!!!) to replace the whole state on a store.

First, let's add a hook in the test suite to run after each test:

// test/js/store.spec.js

const initialState = deepClone(store.state);

describe('store test suite', () => {
    afterEach(() => {
        store.actions.refresh(initialState);
    });

    it('is configured correctly', () => {
        // nothing new here

The deepClone helper was stolen here once again.

The refresh actions just call a mutation

// resources/assets/js/store/actions.js

refresh: 'REFRESH',

And the mutation replace each property of the current state with the initialState we deepCloned in our constant:

// resources/assets/js/store/mutations.js

REFRESH(state, initialState) {
    Object.assign(state, initialState);
},

All tests passes, we can know start testing our store with real examples.

Update: I will find out later that deep nested object breaks this setup. I have to clone the initialState each time before passing it to my refresh() method. I will also extract the function called in the hook so I can reuse it on describe block.

Before my describe block, I have now this function:

// test/js/store.spec.js

const initialState = deepClone(store.state);

const refreshStore = initialState => {
    let applyState = deepClone(initialState);
    store.actions.refresh(applyState);
};

describe('store test suite', () => {
    afterEach(() => refreshStore(initialState));

    it('is configured correctly', () => {
        true.should.be.true;
    });

Ready for real tests

Using TDD, I describe how my store will manage the panels displayed on the screen. These were all simple synchronous mutations, so no mocking involve at all. This is my test suite after a while. I am not really interested in posting about the store code, because it does not really matters. What matters is what my store is able to do, and how I can consume its API in other parts of my application.

// test/js/store.spec.js

describe('panels', () => {
    afterEach(() => refreshStore(initialState));

    it('initially shows a default panel', () => {
        store.state.mainPanel.should.equal('default');
    });
    it('has no sub panels initially', () => {
        store.state.subPanels.should.be.empty;
    });
    it('displays a main panel', () => {
        store.actions.showMainPanel('datatable');
        store.state.mainPanel.should.equal('datatable');
    });
    it('adds a sub panel', () => {
        store.actions.addSubPanel('a subpanel');
        store.state.subPanels.should.have.length(1);
    });
    it('checks if a sub panel is visible', () => {
        store.actions.addSubPanel('a subpanel');
        store.getters.isShowingPanel('a subpanel').should.be.true;
        store.getters.isShowingPanel('nope').should.be.false;
    });
    it('has multiple subpanels', () => {
        store.actions.addSubPanel('a subpanel');
        store.actions.addSubPanel('another subpanel');
        store.state.subPanels.should.have.length(2);
    });
    it('replace a panel if it already exists', () => {
        store.actions.addSubPanel('no duplicate');
        store.actions.addSubPanel('no duplicate');
        store.state.subPanels.should.have.length(1);
    });
    it('displays the latest subpanel first', () => {
        store.actions.addSubPanel('first');
        store.actions.addSubPanel('second');
        store.state.subPanels.indexOf('second').should.equal(0);
    });
    it('closes a subpanel', () => {
        store.actions.addSubPanel('closeMe');
        store.actions.closeSubPanel('closeMe');
        store.state.subPanels.should.be.empty;
    });
});

Hopefully and thanks to TDD, these tests makes sense, and describe what my components will be able to do with the state of my application without further explanations.

Made it!

I wrote my first tests in javascript, with all the benefits of TDD. It reads nicely, made me think about my panels API as I will use it in my components, and gives me the confidence to refactor any line of my store without breaking things. I found the initial setup hard to get started, with all the ES6 syntax compiling, webpack involved, node/browsers environment... I needed a few hours to start to understand where this would lead. I think I have a good starting point to integrate js testing in my workflow now.

Published almost 3 years ago