I am writing unit tests for a pagination module, and it has a simple VueX store module.
I am using Vue.js 2.5 and Mocha/Chai/Sinon for tests. Setup is with Vue CLI 3.
The problem is that when the currentPage is incremented in the store in one unit test, this state persists into the next test even when I try to create a fresh store.
I have attempted to return a fresh pagination module by using a function that returns an Object.assign() fresh copy but this did not work. I have left this in the code as shown in the spec below.
store/pagination.js
const state = {
currentPage: 0
}
export const getters = {
currentPage: state => {
return state.currentPage
}
}
export const actions = {
nextPage ({ commit, state }) {
commit('setCurrentPage', state.currentPage + 1)
}
}
export const mutations = {
setCurrentPage (state, page) {
state.currentPage = page
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
Pagination.spec.js
function getPaginationStore () {
return Object.assign({}, pagination)
}
describe('Paginate.vue', () => {
let localVue
let wrapper
let store
beforeEach(() => {
localVue = createLocalVue()
localVue.use(Vuex)
store = new Vuex.Store({
modules: {
pagination: getPaginationStore()
}
})
wrapper = shallowMount(Pagination, {
localVue,
propsData: {
items: [],
size: 24
},
store
})
})
afterEach(() => {
store = null
})
it('state should be 0', () => {
expect(wrapper.vm.pageNumber).to.equal(0)
wrapper.vm.$store.dispatch('pagination/nextPage')
expect(wrapper.vm.pageNumber).to.equal(1)
})
it('state should be 0 again but is 1', () => {
// THIS TEST FAILS. IT IS ACTUALLY 1
expect(wrapper.vm.pageNumber).to.equal(0)
})
})
The solution was to use a function for the state in the module rather than a plain old javascript object. Here is my new store state code:
export const state = () => {
return {
currentPage: 0
}
}
Answer was provided by #SumNeuron from the Vue discord channel.
Related
Testing a trigger click on a button does not work in Vue using Jest.
When I try to find the button in the wrapper the test passes, but when I try a trigger click on the same button so a method will be called it does not work.
Here is the vue file snapshot of the button:
<v-btn #click="viewAppointment(appointment)" class="ma-2" dark small color="orange" id="view-appointment" data-viewAppointmentBtn>
<v-icon left>mdi-eye</v-icon>
<span>View</span>
</v-btn>
Here is the js file that contains the simple method call::
viewAppointment(appointment) {
this.appointment = appointment;
this.viewAppointmentDialog = !this.viewAppointmentDialog;
},
Here is the .spec.js file for the test::
import './setup.js';
import CoachAppointmentsRequests from '../dashboard/coach/appointments/requests/overview/Overview.vue';
import {shallowMount, createLocalVue} from "#vue/test-utils";
import Vuex from "vuex";
const localVue = createLocalVue();
localVue.use(Vuex);
describe("CoachAppointmentsRequests", () => {
let wrapper;
let store;
let actions;
let state;
let getters;
const $route = {
path: 'appointment/requests/:application_id',
params: { application_id: 123 }
}
actions = {
GET_USER_APPOINTMENTS: jest.fn()
};
state = {
user_appointments: [ {id:1, date: 'May 20, 2020'} ],
all_user_appointments: [ {id:1, date: 'May 20, 2020'} ],
};
getters = {
user_appointments: state => state.user_appointments,
all_user_appointments: state => state.all_user_appointments
};
store = new Vuex.Store({
actions,
getters,
state,
});
const getUserAppointments = jest.fn(() => {
return new Promise(resolve => {
process.nextTick(() => {
resolve({
data: [
{ id:1, appointee_id:2}
]
})
})
})
});
beforeEach(() => {
wrapper = shallowMount(CoachAppointmentsRequests, {
propsData: {},
mocks: {
$route,
},
stubs: {},
methods: {
getUserAppointments,
},
store,
localVue,
});
});
it('click on the view appointment button calls the viewAppointment method', () => {
const viewAppointment = jest.fn();
wrapper.setMethods({ viewAppointment })
const viewAppBtn = wrapper.find('#view-appointment');
viewAppBtn.trigger('click');
expect(viewAppointment).toBeCalled();
});
});
Please I will appreciate your assistance with this issue.
The click handler isn't called immediately after trigger(), but rather it's called in the next tick. However, trigger() returns a Promise that resolves when the component is updated, so you could await the result of the call, as shown in the docs example:
it('clicked it', async () => {
// ...
await viewAppBtn.trigger('click')
expect(viewAppointment).toBeCalled()
})
I had a similar problem. I've used shallowMount to mount vue component and click on button wasn't working. The solution was to change shallowMount to mount.
I'm currently testing vuex module specifically actions.
Here's my code:
store/modules/users.js
export const state = () => ({
users: [],
})
export const mutations = () => ({
SET_USERS(state, users) {
console.log('Should reach Here');
state.users = users
}
})
export const actions = () => ({
getUsers({ commit }) {
return axios.get('/users')
.then(response => {
console.log('Reaching Here');
commit('SET_USERS', response.data.data.results)
})
.catch(error => {
console.log(error);
})
}
})
export const getters = () => {
users(state) {
return state.users;
}
};
Then when I test my actions:
tests/store/modules/users.js
it('should dispatch getUsers', () => {
mock.onGet('/users').reply(200, {
data: {
results: [
{ uid: 1, name: 'John Doe' },
{ uid: 2, name: 'Sam Smith' }
]
},
status: {
code: 200,
errorDetail: "",
message: "OK"
}
});
const commit = sinon.spy();
const state = {};
actions.getUsers({ commit, state });
expect(getters.users(state)).to.have.lengthOf(2);
});
when I try to run the test npm run dev it shows the console.log from action but from mutation SET_USERS it doesn't show the console.log
I'm referring to this documentation which I can use spy using sinon()
https://vuex.vuejs.org/guide/testing.html
How can I access the commit inside action to call mutation SET_USERS?
According to sinon docs
A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls. There are two types of spies: Some are anonymous functions, while others wrap methods that already exist in the system under test.
const commit = sinon.spy();
That is not the 'commit' from Vuex, you should test your mutation individually
actions.getUsers({ commit, state });
The commit argument is actually the spy, it will never trigger the mutation.
To test your mutation it could be something like this
mutations.SET_USERS(state, mockedUsers)
expect(state).to.have.lengthOf(mockedUsers.length)
...
Using Vue CLI I have a unit test that I am trying to check for a true/false that looks like this:
describe('The thing', () => {
it('must be available.', () => {
const available = true
const wrapper = shallowMount(MyVueComponent, {
propsData: { available },
})
expect(wrapper).to.be.true
})
})
When I run npm run test:unit
I get the following:
AssertionError: expected { Object (isFunctionalComponent, _emitted, ...) } to be true
If I just check the value of available, then it's all good. But that seems like I'm doing it wrong.
Other tests I have written are working fine as I am checking for a text value:
describe('The thing', () => {
it('should have a name.', () => {
const name = 'Hal'
const wrapper = shallowMount(MyVueComponent, {
propsData: { name },
})
expect(wrapper.text()).to.include(name)
})
})
I am not sure how to check that the available is a boolean and it must be true. Any suggestions would be appreciated!
EDIT
This is what my Vue component looks like:
export default {
name: 'MyVueComponent',
props: {
name: String
},
data: function() {
return {
available: true,
}
},
}
EDIT 2
This seems to work in my unit test:
describe('The thing', () => {
it('must be available.', () => {
const available = true
const wrapper = shallowMount(MyVueComponent, {
propsData: { available },
})
expect(wrapper.vm.available).to.be.true
})
})
However, it is looking at my actual component in my /src directory. If I change the data values from true to false my tests come out correctly. I'm not sure how to have the data stay at the test level. So if I were to change const available = false, my test should fail--but it does not.
EDIT 3
It seems like this works (to access the data object):
describe("The thing", () => {
it("must be available.", () => {
const defaultData = MyVueComponent.data();
// const wrapper = shallowMount(MyVueComponent, {});
expect(defaultData.available).to.be.true;
});
});
But it still seems not right that I'm referencing my actual code, and not sticking within the unit tests.
You want to check the received prop, which you can do with wrapper.props()
describe('The thing', () => {
it('must be available.', () => {
const available = true
const wrapper = shallowMount(MyVueComponent, {
propsData: { available },
})
expect(wrapper.props().available).to.be.true
// or:
// expect(wrapper.props().available).to.equal(available)
})
})
Chai's .to.be.true and .to.equal use === so there is no need to separately check that it is indeed a Boolean, but if you prefer the "expressiveness" of it, you can check it too:
expect(wrapper.props().available).to.be.a('boolean')
I'm trying to unit test a component method. The question here does not lay out how to access the component method from a unit test.
Specifically, given my Vue component below, how do I access doSomeWork() from my unit test?
Vue component:
<template>
<div id="ThisStuff">
<span>
Some other stuff is going on here
</span>
</div>
</template>
<script>
import foo from 'bar'
export default {
props: {
ObjectWithStuffInIt: [
{
id: 1
bar: false
},
{
id: 2
bar: false
},
]
},
data: {
foo: "foo"
},
methods: {
doSomeWork: function() {
for (var i = 0; i < ObjectWithStuffInIt.length; i++) {
if (foo === "diddly") {
ObjectWithStuffInIt[i].bar = true;
}
}
}
}
}
</script>
My test code:
import {createLocalVue, shallow} from 'vue-test-utils'
import ThisVueFile.test.js from '../../thisPlace/ThatPlace/ThisVueFile.vue'
import Vuex from 'vuex'
const localVue = createLocalVue()
localVue.use(Vuex);
describe('ThisVueFile.test.js', () => {
let user;
let store;
beforeEach(() => {
let getters = {
user: () => user
}
store = new Vuex.Store({ getters })
})
// I need to fill propsData: with some local data here
// because it is server data
// I need to have access to the method
// I need to use local data for `foo` in the test.
it(' When foo is set to -diddly- then set bar to true ', () => {
foo = "diddly";
// run the method in the component here
doSomeWork();
expect(OjbectWithStuffInIt[0].bar.equals(true));
})
})
Calling component method
The wrapper provides access to the component instance via its vm property, so you could call the method directly with:
wrapper.vm.doSomeWork()
Setting props
The mounting options (passed to shallowMount() or mount()) include the propsData property that could be used to initialize the component's props before mounting.
You could also use the wrapper's setProps() after the component has already been mounted.
Example:
it('...', () => {
const wrapper = shallowMount(MyComponent, {
propsData: {
myItems: [
{ id: 200, bar: false },
{ id: 300, bar: false }
]
}
});
// OR
wrapper.setProps({
myItems: [
{ id: 400: bar: true }
]
})
})
Modifying component data property
The mounting options includes the data property that could be used to initialize the component's data before mounting.
You could also use the wrapper's setData() after the component has already mounted.
You could access the component's data property directly through the wrapper's vm property.
Example:
it('...', () => {
const wrapper = shallowMount(MyComponent, {
data() {
return {
foo: 1
}
}
});
// OR
wrapper.setData({ foo: 2 })
// OR
wrapper.vm.foo = 3
})
Full example
Altogether, your test might look similar to this:
import { createLocalVue, shallowMount } from '#vue/test-utils'
import MyComponent from '#/components/MyComponent'
describe('MyComponent', () => {
it('When foo is set to -something-, set bar to true', () => {
const myItems = [
{ id: 200, bar: false },
{ id: 300, bar: false }
]
const localVue = createLocalVue()
const wrapper = shallowMount(MyComponent, {
localVue,
propsData: {
myItems
}
})
wrapper.vm.foo = 'something'
wrapper.vm.doSomeWork()
expect(myItems[0].bar).toBe(true)
})
})
demo
I'm testing a Single file component that uses vue router to watch $route. The problem is that I can't get the test to both change the route and trigger the watcher's function.
The test file:
import { createLocalVue, shallow } from 'vue-test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
const localVue = createLocalVue();
localVue.use(Vuex);
const $route = {
path: '/my/path',
query: { uuid: 'abc' },
}
wrapper = shallow({
localVue,
store,
mocks: {
$route,
}
});
it('should call action when route changes', () => {
// ensure jest has a clean state for this mocked func
expect(actions['myVuexAction']).not.toHaveBeenCalled();
vm.$set($route.query, 'uuid', 'def');
//vm.$router.replace(/my/path?uuid=def') // tried when installing actual router
//vm.$route.query.uuid = 'def'; // tried
//vm.$route = { query: { uuid: 'def'} }; // tried
expect(actions['myVuexAction']).toHaveBeenLastCalledWith({ key: true });
});
My watch method in the SFC:
watch: {
$route() {
this.myVuexAction({ key: true });
},
},
How do you mock router in such a way that you can watch it and test the watch method is working as you expect?
This is how I'm testing a watch on route change that adds the current route name as a css class to my app component:
import VueRouter from 'vue-router'
import { shallowMount, createLocalVue } from '#vue/test-utils'
import MyApp from './MyApp'
describe('MyApp', () => {
it('adds current route name to css classes on route change', () => {
// arrange
const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter({ routes: [{path: '/my-new-route', name: 'my-new-route'}] })
const wrapper = shallowMount(MyApp, { localVue, router })
// act
router.push({ name: 'my-new-route' })
// assert
expect(wrapper.find('.my-app').classes()).toContain('my-new-route')
})
})
Tested with vue#2.6.11 and vue-router#3.1.3.
I checked how VueRouter initializes $route and $router and replicated this in my test. The following works without using VueRouter directly:
const localVue = createLocalVue();
// Mock $route
const $routeWrapper = {
$route: null,
};
localVue.util.defineReactive($routeWrapper, '$route', {
params: {
step,
},
});
Object.defineProperty(localVue.prototype, '$route', {
get() { return $routeWrapper.$route; },
});
// Mock $router
const $routerPushStub = sinon.stub();
localVue.prototype.$router = { push: $routerPushStub };
const wrapper = shallowMount(TestComponent, {
localVue,
});
Updating $route should always be done by replacing the whole object, that is the only way it works without using a deep watcher on $route and is also the way VueRouter behaves:
$routeWrapper.$route = { params: { step: 1 } };
await vm.wrapper.$nextTick();
Source: install.js
Its working for me
let $route = {
name: 'any-route',
};
We defined a $route and we called like
wrapper = mount(YourComponent, {
mocks: {
$route,
},
});
and my componente is like this
#Watch('$route', { deep: true, immediate: true, })
async onRouteChange(val: Route) {
if (val.name === 'my-route') {
await this.getDocumentByUrl();
await this.allDocuments();
}
};
pd: I use typescript, but this work with the another format
and finally my test
it('my test', ()=>{
const getDocumentByUrl = jest.spyOn(wrapper.vm, 'getDocumentByUrl');
const allDocuments = jest.spyOn(wrapper.vm, 'allDocuments');
wrapper.vm.$route.name = 'my-route';
await flushPromises();
expect(getDocumentByUrl).toHaveBeenCalled();
expect(allDocuments).toHaveBeenCalled();
})
The way to do this actually is to use vue-test-utils wrapper method, setData.
wrapper.setData({ $route: { query: { uuid: 'def'} } });