Testing Vuex stores without components - vuex

I've been looking through https://vue-test-utils.vuejs.org/guides/#testing-key-mouse-and-other-dom-events to learn how to write tests using Jest combined with vue-test-utils. However, these examples all use a shallowMount where you mount the component, but I want to avoid that as I use GUI tests to test the actual components.
However, I'm having issues doing this without the component. In the examples from vue-test-utils they use shallowMount to inject the store, but as I'm not doing that I can't access my store, which means that my state, my getters etc all are undefined when testing.
My store testing file myStore.spec.js:
import { createLocalVue } from '#vue/test-utils';
import myStore from './myStore';
import Vuex from 'vuex';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('myStore tests', () => {
let actions;
let getters;
let store;
beforeEach(() => {
actions = {
setInUse: jest.fn(),
};
getters = {
isInUse: () => false,
};
store = new Vuex.Store({
modules: {
myStore: {
state: {
isInUse: false,
},
actions,
getters: myStore.getters,
},
},
});
});
it('example test', () => {
actions.setInUse(true);
expect(getters.isInUse(state)).toBeTruthy();
});
});
and one of the errors when running this test is: state is not defined.
Where am I going wrong? How should I access my store in my tests? Ideally I want to set my test store to the same as what I import from myStore, but I'm not sure that works?

Related

Vue3 testing composition API with vuex in vitest

I'm having trouble getting a mock action to run using Vue3 while testing with vitest.
I have a component which calls out to a modularized vuex store that is imported into my component using the composition api. Something like the following.
export default defineComponent({
setup() {
const { doAction } = useModActions([
'doAction'
])
}
})
I use createNamespacedHelpers to setup my store module from the vuex-composition-helpers library.
After I use useStore with a Symbol key to setup the state of my store. I consume it in my application by doing
app.use(store, key)
To mock it in my tests I was trying the following
const actions = {
doAction: vi.fn()
}
const spy = vi.spyOn(actions, 'doAction')
const mockStore = createStore({
modules: {
mod: {
namespaced: true,
actions
}
}
})
const wrapper = mount(Component, {
global: {
provide: { [key]: mockStore }
}
})
But my spy is never called and my component always calls the original implementation. Is there a way to get all these pieces working together?
The mockStore here (from Vuex's createStore()) is an instance of a Vue plugin, which should be passed to the global.plugins mounting option (not global.provide):
// MyComponent.spec.js
import { describe, it, expect, vi } from 'vitest'
import { mount } from '#vue/test-utils'
import { createStore } from 'vuex'
import MyComponent from '../MyComponent.vue'
describe('MyComponent', () => {
it('button calls doAction', async () => {
const actions = {
doAction: vi.fn(),
}
const mockStore = createStore({
modules: {
myModule: {
namespaced: true,
actions,
},
},
})
const wrapper = mount(MyComponent, {
global: {
plugins: [mockStore], // šŸ‘ˆ
},
})
await wrapper.find("button").trigger("click")
expect(actions.doAction).toHaveBeenCalled()
})
})
demo

Vuex unable to use modules

I am using Vue with Webpacker with Rails. I am having some problem with Vuex, specifially on using modules.
application.js:
import store from '../store/store'
Vue.prototype.$store = store;
document.addEventListener('turbolinks:load', () => {
axios.defaults.headers.common['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
const app = new Vue({
el: '[data-behavior="vue"]',
store
})
})
store.js:
import Vue from 'vue/dist/vue.esm'
import Vuex from 'vuex';
import axios from 'axios';
import itemstore from'./modules/itemstore'
Vue.use(Vuex)
import VueAxios from 'vue-axios'
Vue.use(VueAxios, axios)
const store = new Vuex.Store({
............
modules: {
itemstore
}
})
export default store;
itemstore.js:
import axios from 'axios';
const itemstore = {
state: {
items: [],
},
actions: {
loadItems ({ commit }) {
axios
.get('/items.json')
.then(r => r.data)
.then(items => {
commit('SET_ITEMS', items);
})
}
},
mutations: {
SET_ITEMS (state, items) {
state.items = items
}
},
}
export default itemstore;
In my component:
mounted () {
this.$store.dispatch('loadItems')
},
computed: {
...mapState([
'items'
]),
}
First to get the main store imported I need Vue.prototype.$store = store;
Secondly once i move those states, actions and mutations from store.js to itemstore.js, items gets undefined. What am I doing wrong?
The namespaced setting will cause the actions, mutations and setters of a store to be namespaced based on the module name. The state of a module, however, is always separated off into its own subtree within state, even if namespacing is not being used.
So this won't work:
...mapState([
'items'
]),
This is looking for an items property in the root state.
Instead you can use something like:
...mapState({
items: state => state.itemstore.items
})
You might be tempted to try to write it like this:
...mapState('itemstore', ['items'])
However, passing the module name as the first argument to mapState will only work with namespaced modules.

Can't seem to test Vuex Mutation in Vue Component

What should be pretty simple seems to be alluding me...
I have a vue component that when the correct html tag is clicked, a method is run that triggers a commit mutation.
All I want to do is see that the mutation is indeed triggered in the component. I can see the method get triggered but not the mutation that the method calls.
I am using Jest for testing
Goal: Verify the 'setEditUser' commit is called.
Dashboard
...
<tr
id="editUserTr"
class="col1"
v-for="listUserĀ inĀ users"
v-bind:key="listUser.email"
#click="editUser(listUser)"
>
<td>{{listUser.id}}</td>
<td>{{listUser.first_name}}</td>
...
methods: {
editUser(listUser) {
this.$store.commit("setEditUser", listUser);
this.$router.push("/editUser");
},
dashboardTest.spec.js
import { shallowMount, createLocalVue } from '#vue/test-utils';
import Dashboard from '../../src/components/Dashboard.vue';
import VueRouter from 'vue-router';
import Vuex from 'vuex';
const fb = require('../../src/firebaseConfig.js');
const sinon = require('sinon');
const localVue = createLocalVue();
localVue.use(Vuex, VueRouter);
describe('Dashboard.js', () => {
beforeEach(() => {
const mutations = {
setEditUser: jest.fn(),
};
const getters = {
userProfile: () => jest.fn(),
users: () => jest.fn(),
};
const $router = {
push: jest.fn(),
};
const store = new Vuex.Store({ mutations, getters });
});
test('Edit User function does not error out', () => {
const userProfile = {
role: ['admin'],
};
const wp = shallowMount(Dashboard, {
store,
localVue,
mocks: {
$router,
},
computed: {
userProfile() {
return userProfile;
},
users() {
return {
users: {
id: 'someEmail#email.com',
first_name: 'Stub',
},
};
},
},
});
const userInfo = wp.vm.users;
const stub = sinon.stub(wp.vm, 'editUser');
wp.findAll('td').at(0).trigger('click');
expect(mutations.setEditUser).toHaveBeenCalledWith({}, {userInfo}) // Fails. Says: Number of calls = 0
expect(stub.callCount).toBe(1);// Passes
});
});
Thanks for your help!
Edit:
Figured it out....
When I create this constant
const stub = sinon.stub(wp.vm, 'editUser');
I am messing with how the commit and router get called. Probably not passing in the proper arguments to the stub. I tried adding the proper arguments to the stub but still no go.
If I remove the stub then I can verify that the commit and push methods get called.
So... I created another unit test that just test that. Its more code then what I would like but I did solve my issue for now :)

Vue Test Mock Promise-Based Action within Module

I unfortunately can't attach all code or create a gist because the project I'm working on is related to work but I can give enough detail that I think it will work.
I'm trying to mock a call to an action that is stored in a different module but for the life of me I can't figure out how to. I'm able to create a Jest spy on the store.dispatch method but I want to be able to resolve the promise and make sure that the subsequent steps are taken.
The method in the SFC is
doSomething(data) {
this.$store.dispatch('moduleA/moduleDoSomething',{data: data})
.then(() => {
this.$router.push({name: 'RouteName'})
})
.catch(err => {
console.log(err)
alert('There was an error. Please try again.')
})
},
This is what my test looks like:
import { mount, createLocalVue } from '#vue/test-utils'
import Vuex from 'vuex'
import Vuetify from 'vuetify'
import Component from '#/components/Component'
import moduleA from '#/store/modules/moduleA'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Vuetify)
describe('Component.vue', () => {
let actions
let store
const $router = []
beforeEach(() => {
actions = {
moduleDoSomething: jest.fn((payload) => {
return Promise.resolve({
status: 200,
})
})
}
store = new Vuex.Store({
state: {},
modules: {
moduleA: {
actions
}
},
})
})
it('does something', () => {
const wrapper = mount(Component, {
store,
localVue,
mocks: {
$router,
},
})
let button = wrapper.find('button that calls doSomething')
button.trigger('click')
expect(actions.moduleDoSomething).toHaveBeenCalled()
expect(wrapper.vm.$router[0].name).toBe('RouteName')
})
})
The following test passes, but I don't want to just test that the action was dispatched; I also want to test things in the "then" block.
it('does something', () => {
const dispatchSpy = jest.spyOn(store, 'dispatch')
const wrapper = mount(Component, {
store,
localVue,
mocks: {
$router,
},
})
let button = wrapper.find('button that calls doSomething')
button.trigger('click')
expect(dispatchSpy).toHaveBeenCalledWith('moduleA/moduleDoSomething',{data: data})
})
})
I managed to solve this problem by simply making the module namespaced in the mocked store.
store = new Vuex.Store({
state: {},
modules: {
moduleA: {
actions,
namespaced: true
}
},
})
I'll delete the question in a little bit

How to write test that mocks the $route object in vue components

I have a component that contains statement like this.$route.fullPath, how should I mock value of fullPathof $route object if I want to test that component?
I disagree with the top answer - you can mock $route without any issue.
On the other hand, installing vue-router multiple times on the base constructor will cause you problems. It adds $route and $router as read only properties. Which makes it impossible to overwrite them in future tests.
There are two ways to achieve this with vue-test-utils.
Mocking vue-router with the mocks option
const $route = {
fullPath: 'full/path'
}
const wrapper = mount(ComponentWithRouter, {
mocks: {
$route
}
})
wrapper.vm.$route.fullPath // 'full/path'
You can also install Vue Router safely by using createLocalVue:
Installing vue-router safely in tests with createLocalVue
const localVue = createLocalVue()
localVue.use(VueRouter)
const routes = [
{
path: '/',
component: Component
}
]
const router = new VueRouter({
routes
})
const wrapper = mount(ComponentWithRouter, { localVue, router })
expect(wrapper.vm.$route).to.be.an('object')
Best not mock vue-router but rather use it to render the component, that way you get a proper working router. Example:
import Vue from 'vue'
import VueRouter from 'vue-router'
import totest from 'src/components/totest'
describe('totest.vue', () => {
it('should totest renders stuff', done => {
Vue.use(VueRouter)
const router = new VueRouter({routes: [
{path: '/totest/:id', name: 'totest', component: totest},
{path: '/wherever', name: 'another_component', component: {render: h => '-'}},
]})
const vm = new Vue({
el: document.createElement('div'),
router: router,
render: h => h('router-view')
})
router.push({name: 'totest', params: {id: 123}})
Vue.nextTick(() => {
console.log('html:', vm.$el)
expect(vm.$el.querySelector('h2').textContent).to.equal('Fred Bloggs')
done()
})
})
})
Things to note:
I'm using the runtime-only version of vue, hence render: h => h('router-view').
I'm only testing the totest component, but others might be required if they're referenced by totest eg. another_component in this example.
You need nextTick for the HTML to have rendered before you can look at it/test it.
One of the problems is that most of the examples I found referred to the old version of vue-router, see the migrations docs, eg. some examples use router.go() which now doesn't work.
No answer was helping me out, So I dig into vue-test-utils documentation and found myself a working answer, so you need to import.
import { shallowMount,createLocalVue } from '#vue/test-utils';
import router from '#/router.ts';
const localVue = createLocalVue();
We created a sample vue instance. While testing you need to use shallowMount so you can provide vue app instance and router.
describe('Components', () => {
it('renders a comment form', () => {
const COMMENTFORM = shallowMount(CommentForm,{
localVue,
router
});
})
})
You can easily pass router and to shallow mount and it does not gives you the error. If you want to pass store you use:
import { shallowMount,createLocalVue } from '#vue/test-utils';
import router from '#/router.ts';
import store from '#/store.ts';
const localVue = createLocalVue();
And then pass store:
describe('Components', () => {
it('renders a comment form', () => {
const COMMENTFORM = shallowMount(CommentForm,{
localVue,
router,
store
});
})
})
This solution solved the following errors:
Cannot read property 'params' of undefined when using this.$route.params.id
Unknown custom element router-link
āœ”
Easiest method i found is to use localVue
import { createLocalVue, mount } from '#vue/test-utils';
import VueRouter from 'vue-router';
import Vuex from 'vuex';
import ComponentName from '#/components/ComponentName.vue';
// Add store file if any getters is accessed
import store from '#/store/store';
describe('File name', () => {
const localVue = createLocalVue();
localVue.use(VueRouter);
// Can also be replaced with route(router.js) file
const routes = [
{
path: '/path',
component: ComponentName,
name: 'Route name'
}
];
const router = new VueRouter({ routes });
// if needed
router.push({
name: 'Route name',
params: {}
});
const wrapper = mount(ComponentName, {
localVue,
router,
store
});
test('Method()', () => {
wrapper.vm.methodName();
expect(wrapper.vm.$route.path)
.toEqual(routes[0].path);
});
});
Hope it helps!!!
Why are all answers so complicated? You can just do:
...
wrapper = mount(HappyComponent, {
mocks: {
$route: { fullPath: '' }
},
})
...
You dont have to specifically "mock" a router. Your application can set VueRouter in the global vue scope and you can still make it do what you want in your tests without issue.
Read the localVue usage with VueRouter: https://vue-test-utils.vuejs.org/guides/#using-with-vue-router.
I am currently pulling in a complex router from our main app and am able to jest.spyOn() calls to router.push() as well as setting the path before the component is created running shallowMount() for some route handling in a created() hook.
The Workaround
// someVueComponent.vue
<template>
... something
</template>
<script>
...
data () {
return {
authenticated: false
}
},
...
created () {
if(!this.authenticated && this.$route.path !== '/'){
this.$router.push('/')
}
}
</script>
// someVueComponent.spec.js
import Vuex from 'vuex'
import VueRouter from 'vue-router'
import { shallowMount, createLocalVue } from '#vue/test-utils'
import SomeVueComponent from 'MyApp/components/someVueComponent'
import MyAppRouter from 'MyApp/router'
import MyAppCreateStore from 'MyApp/createStore'
import merge from 'lodash.merge'
function setVueUseValues (localVue) {
localVue.use(Vuex)
localVue.use(VueRouter)
// other things here like custom directives, etc
}
beforeEach(() => {
// reset your localVue reference before each test if you need something reset like a custom directive, etc
localVue = createLocalVue()
setVueUseValues(localVue)
})
let localVue = createLocalVue()
setVueUseValues(localVue)
test('my app does not react to path because its default is "/"', () => {
const options = {
localVue,
router: MyAppRouter,
store: MyAppCreateStore()
}
const routerPushSpy = jest.spyOn(options.router, 'push')
const wrapper = shallowMount(SomeVueComponent, options)
expect(routerPushSpy).toHaveBeenCalledTimes(0)
})
test('my app reacts to path because its not "/" and were not authenticated', () => {
const options = {
localVue,
router: MyAppRouter,
store: MyAppCreateStore()
}
const routerPushSpy = jest.spyOn(options.router, 'push')
options.router.push('/nothomepath')
expect(routerPushSpy).toHaveBeenCalledWith('/nothomepath') // <- SomeVueComponent created hook will have $route === '/nothomepath' as well as fullPath
const wrapper = shallowMount(SomeVueComponent, options)
expect(routerPushSpy).toHaveBeenCalledWith('/') // <- works
})
The above is done with the idea that I need the $route state changed before SomeVueComponent.vue is created/mounted. Assuming you can create the wrapper and want to test that the component this.$router.push('/something') based on some other state or action you can always spy on the wrapper.vm instance
let routerPushSpy = jest.spyOn(wrapper.vm.$router, 'push') // or before hooks, etc
As of this writing there seems to be an open defect which keeps the following from working because vm.$route will always be undefined, making the above the only option (that I know of) as there is no other way to "mock" the $route because installing VueRouter writes read only properties to $route.
From the vue-test-utils docs https://vue-test-utils.vuejs.org/guides/#mocking-route-and-router:
import { shallowMount } from '#vue/test-utils'
const $route = {
path: '/some/path'
}
const wrapper = shallowMount(Component, {
mocks: {
$route
}
})
wrapper.vm.$route.path // /some/path
If your interested here is the github link to a reproduction of the issue: https://github.com/vuejs/vue-test-utils/issues/1136
All kudos to #SColvin for his answer; helped find an answer in my scenario wherein I had a component with a router-link that was throwing a
ERROR: '[Vue warn]: Error in render function: (found in <RouterLink>)'
during unit test because Vue hadn't been supplied with a router. Using #SColvin answer to rewrite the test originally supplied by vue-cli from
describe('Hello.vue', () =>
{
it('should render correct contents', () =>
{
const Constructor = Vue.extend(Hello);
const vm = new Constructor().$mount();
expect(vm.$el.querySelector('.hello h1').textContent)
.to.equal('Welcome to Your Vue.js App');
});
to
describe('Hello.vue', () =>
{
it('should render correct contents', () =>
{
Vue.use(VueRouter);
const router = new VueRouter({
routes: [
{ path: '/', name: 'Hello', component: Hello },
],
});
const vm = new Vue({
el: document.createElement('div'),
/* eslint-disable object-shorthand */
router: router,
render: h => h('router-view'),
});
expect(vm.$el.querySelector('.hello h1').textContent)
.to.equal('Welcome to Your Vue.js App');
});
});
Not needing to pass parameters in to the view I could simplify the component as the default render, no need to push and no need to wait nextTick. HTH someone else!
Adding to the great answer from #SColvin, here's an example of this working using Avoriaz:
import { mount } from 'avoriaz'
import Vue from 'vue'
import VueRouter from 'vue-router'
import router from '#/router'
import HappyComponent from '#/components/HappyComponent'
Vue.use(VueRouter)
describe('HappyComponent.vue', () => {
it('renders router links', () => {
wrapper = mount(HappyComponent, {router})
// Write your test
})
})
I believe this should work with vue-test-utils, too.
Take a look at this example using vue-test-utils, where I'm mocking both router and store.
import ArticleDetails from '#/components/ArticleDetails'
import { mount } from 'vue-test-utils'
import router from '#/router'
describe('ArticleDetails.vue', () => {
it('should display post details', () => {
const POST_MESSAGE = 'Header of our content!'
const EXAMPLE_POST = {
title: 'Title',
date: '6 May 2016',
content: `# ${POST_MESSAGE}`
}
const wrapper = mount(ArticleDetails, {
router,
mocks: {
$store: {
getters: {
getPostById () {
return EXAMPLE_POST
}
}
}
}
})
expect(wrapper.vm.$el.querySelector('h1.post-title').textContent.trim()).to.equal(EXAMPLE_POST.title)
expect(wrapper.vm.$el.querySelector('time').textContent.trim()).to.equal(EXAMPLE_POST.date)
expect(wrapper.vm.$el.querySelector('.post-content').innerHTML.trim()).to.equal(
`<h1>${POST_MESSAGE}</h1>`
)
})
})
This is what I've been doing as per this article:
it('renders $router.name', () => {
const scopedVue = Vue.extend();
const mockRoute = {
name: 'abc'
};
scopedVue.prototype.$route = mockRoute;
const Constructor = scopedVue.extend(Component);
const vm = new Constructor().$mount();
expect(vm.$el.textContent).to.equal('abc');
});
You can mock to vm.$router by setting vm._routerRoot._router
For example
var Constructor = Vue.extend(Your_Component)
var vm = new Constructor().$mount()
var your_mock_router = {hello:'there'}
vm.$router = your_mock_router //An error 'setting a property that has only a getter'
vm._routerRoot._router = your_mock_router //Wow, it works!
You can double check their source code here: https://github.com/vuejs/vue-router/blob/dev/dist/vue-router.js#L558
Easiest way i've found is to mock the $route.
it('renders $router.name', () => {
const $route = {
name: 'test name - avoriaz'
}
const wrapper = shallow(Component, {
mocks: {
$route
}
})
expect(wrapper.text()).to.equal($route.name)
})