Vuex Test Service Mocking

A typical pattern for Vue applications which use REST APIs is to have a Vuex Store which delegates API operations to a service helper. This decouple the store’s logic for client-side data storage/manipulation logic, from logic to interact with the server.

However, this can make unit testing the Vuex Store logic harder as now there is a dependency on this service helper which requires a server to work with. To test this you could start the server and have the service use it, but its not always practical and certainly isn’t unit testing.

A solution to this is to use a test double of some kind for the service.

Pretty much all tutorials I’ve read on this have used modules containing a bunch of functions for the services; then the test code mocks this module and some how injects it into the environment such that its used by the service code.

This all seemed quite complicated, relied on test frameworks to do special work, and it wasn’t clear how to integrate Typescript for extra type-safety checks.

Here is my alternative approach.

The following snippets are from a simple Vue application that displays randomly selected ‘inspirational quotes’ which the Vuex Store loads from an injected service. The application contains unit tests for the store and a component that displays the quotes. Typescript is used for extra safety, although its not strictly necessary.

Firstly, create an interface for your service with all the operations that can be performed but with no implementation details.

export interface InspirationalQuoteService {

    loadQuote(): Promise<string>

}

Second, when creating the Vuex Store, inject the service to be used rather than importing the implementation. This follows the Dependency Inversion Principle.

export function createStore (service: InspirationalQuoteService) {
  return new Vuex.Store({
    state: {
      quotes: []
    },
    mutations: {
      REPLACE_QUOTES (state: State, quotes: Array<string>) {
        state.quotes = quotes
      }
    },
    actions: {
      loadQuotes: async (context: ActionContext<State, RootState>, amount: number) => {
        const quotes = []
        for (let i = 0; i < amount; ++i) {
          const quote = await service.loadQuote()
          quotes.push(quote)
        }
        context.commit('REPLACE_QUOTES', quotes)
      }
    },
    getters: {
      quotes (state: State) {
        return state.quotes
      }
    }
  })
}

Next, create a type-safe proxy for the Vuex Store. This encapsulates interactions with the store, provides type-safety checks and a more elegant API. Alternatives could be to use direct-vuex or vuex-class-component. However, I quite like the simplicity and readability of this approach.

import { Store } from 'vuex'
import { RootState } from '@/store'

export default class StoreProxy {
    private store: Store<RootState>

    constructor (store: Store<RootState>) {
      this.store = store
    }

    async loadQuotes (amount: number) {
      await this.store.dispatch('loadQuotes', amount)
    }

    get quotes () {
      return this.store.getters.quotes
    }
}

To test your store, simply provide a mock service. Again, this can be done a number of ways, but I find using Substitute to be an elegant way to do this in Typescript code.

function createStoryProxy (inspirationalQuotesService: InspirationalQuoteService) {
  const store = createStore(inspirationalQuotesService)
  const storeProxy = new StoreProxy(store)
  return storeProxy
}

describe('Store', () => {
  it('When initially created, then quotes is empty', async () => {
    const inspirationalQuotesService = Substitute.for<InspirationalQuoteService>()
    const storeProxy = createStoryProxy(inspirationalQuotesService)

    expect(storeProxy.quotes).toHaveLength(0)
  })
})

Substitute provides a simple API for mocking functions and Promises.

  it('When quotes loaded successfully, then quotes can be read', async () => {
    const inspirationalQuotesService = Substitute.for<InspirationalQuoteService>()
    inspirationalQuotesService.loadQuote().resolves('Quote')
    const storeProxy = createStoryProxy(inspirationalQuotesService)

    await storeProxy.loadQuotes(5)

    expect(storeProxy.quotes).toHaveLength(5)
  })

And can even mock rejected Promises.

  it('When quote loading fails, then quotes state not altered', async () => {
    const inspirationalQuotesService = Substitute.for<InspirationalQuoteService>()
    inspirationalQuotesService.loadQuote().rejects(new Error())
    const storeProxy = createStoryProxy(inspirationalQuotesService)

    await expect(storeProxy.loadQuotes(5)).rejects.toThrow()

    expect(storeProxy.quotes).toHaveLength(0)
  })

The approach can be integrated with Vue Test Utils for component testing. Note the use of flush-promises to wait for all Promises to be resolved before checking the component’s rendered correctly.

  it('When created, then the loaded quotes are included in block quotes', async () => {
    const inspirationalQuotesService = Substitute.for<InspirationalQuoteService>()
    inspirationalQuotesService.loadQuote().resolves('Quote 1', 'Quote 2', 'Quote 3', 'Quote 4', 'Quote 5')
    const store: Store<RootState> = createStore(inspirationalQuotesService)

    const wrapper = mount(InspirationalQuotes, {
      localVue,
      store
    })
    await flushPromises()

    const children: WrapperArray<Vue> = wrapper.findAll('blockquote')
    for (let i = 0; i < 5; i++) {
      expect(children.at(i).text()).toEqual(`Quote ${i + 1}`)
    }
  })

Finally, provide the concrete implementation of the service when creating the Vuex Store for the real Vue application.

import Vue from 'vue'
import Vuex from 'vuex'
import App from './App.vue'
import { createStore } from '@/store/'
import { InspirationalQuoteServiceImpl } from './services/impl/InspirationalQuoteServiceImpl'

Vue.config.productionTip = false
Vue.use(Vuex)

const inspirationalQuotesService = new InspirationalQuoteServiceImpl()
const store = createStore(inspirationalQuotesService)

new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

In reality, this is nothing new and is just applying techniques from Object-Oriented programming to the problem. It has its limitations, for example its quote verbose given the Typescript code plus StoreProxy can be fragile. However, its not an approach I’ve seen from the Vue community so far.

Full source code