tl;dr: Scroll down to Conclusion immediately
- In Storybook, each story is a new Vue app
⇒ register Vuex plugin on Vue prototype
- In Vue Test Utils, each test reuses the existing Vue constructor
⇒ register Vuex plugin on local Vue instance
This blog post demonstrates how to set up a simple unit test and story for a component that is connected to a Vuex store. These components usually are smart/container components.
In my current project assignment I have the honor to help 4 teams starting to improve code quality and to release more often. Unfortunately there are not a lot of dumb components, which are easier to test and to write stories for.
There is no safety net (tests + stories) created yet, so refactoring is kind of risky. The perfect Chicken and Egg problem! How can I still test these components? How do I mock the Vuex store?
Since this is a VueJS / Nuxt project, we started using Vue Test Utils with Jest.
I also opted to start using Storybook – especially for the common components.
The example component for this blog post is called s-breadcrumbs, which is just a wrapper around v-breadcrumbs.
Unfortunately it is connected to the global store directly, so I have to mock the store.
The documentation for Jest and Storybook is kind of clear how to set up everything up, but still I had some issues with mocking the Vuex store. There seem to be some differences in how you do this in Storybook compared to Vue Test Utils.
It’s all in the docs but if you didn’t read them carefully you will be frustrated for hours like me.
Unit testing s-breadcrumbs
Our component gets
this.$store.getters.breadcrumbs (which is in fact a computed property) and passes this value into v-breadcrumbs using the
items property. There’s of course is more happening in our component, but it isn’t relevant.
Here is a simple unit test stub:
Similar to vue-router, we have to set up unit testing with Vuex by not calling
Vue.use(Vuex) in the test setup, but we have to use a localVue instance. This way we prevent leaking test setup to other tests, so that other tests are not affected by the current test.
There is some boilerplate involved here, and we don’t want to repeat ourselves, so I extracted the store and localVue stuff into a mock helper that can be imported in all tests where the Vuex store should be mocked:
Story for s-breadcrumbs
Writing stories usually is less work than writing unit tests, because you don’t have to write assertions. Just set up some stories (probably containing some knobs) and for the rest the user is left with a nice interactive component running in the browser. Therefore I always want to encourage developers to write stories in parallel with unit tests.
Here is a simple story for our s-breadcrumbs component:
Remember that the component expects a store, so this will throw an error:
this.$store is undefined. Let’s mock the store, else it is not available in the component.
We are getting close, the store is mocked, but the Vuex plugin is not yet loaded into Vue. We have to call
.storybook/config.js. This will add the
$store property on all Vue components, because all components prototypical inherit from
Here is the gist: this is an important difference from unit testing because here we are not running some component in isolation. We are running a full fledged Vue app containing just one simple component tree instead. The root of this tree is the component defined as storybook configuration. The first child is the component we have writting the story for: s-breadcrumbs.
Since our component needs access to the store, we have to inherit it from its parent.
If we would initialize Vuex on the
localVue instance, the mock store would be only available on the storybook component, not on s-breadcrumbs.
There are 2 main differences between mocking the Vuex store in Vue Test Utils and Storybook.
- In Storybook you just call
Vue.use(Vue)in the storybook config file and adding a mock store to the
storeproperty in the story. This is similar to the application setup. In fact, a story is just a small application.
- In Vue Test Utils you are compiling and mounting the component directly so the mocked store is defined on the
storeproperty and as result it will be available in the component under
this.$store. You have to setup Vuex on a local copy of Vue to prevent making changes to the global
Vueconstructor and pass this Vue instance via the mounting options together with the mock store.