How to test Vue Component method call within an async method - vue.js

I believe I am struggling to properly mock my methods here. Here is my situation, I have a component with two methods;
name: 'MyComponent',
methods: {
async submitAction(input) {
// does await things
// then ...
this.showToastMessage();
},
showToastMessage() {
// does toast message things
},
}
And I want to write a test that will assert that showToastMessage() is called when submitAction(input) is called. My basic test looking something like this;
test('the toast alert method is called', () => {
let showToastMessage = jest.fn();
const spy = jest.spyOn(MyComponent.methods, 'showToastMessage');
const wrapper = shallowMount(MyComponent, { localVue });
const input = // some input data
wrapper.vm.submitAction(input); // <--- this calls showToastMessage
expect(spy).toHaveBeenCalled();
};
NOTE: localVue is declare as such at the top of the file const localVue = createLocalVue();
I confirmed that both submitAction() and showToastMessage() methods are being called during the tests, by sneaking a couple of console.log()'s and observing it in the test output, however the test still fails;
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: called with 0 arguments
Number of calls: 0
566 | const wrapper = shallowMount(MyComponent, { localVue } );
567 | wrapper.vm.submitAction(input);
> 568 | expect(spy).toHaveBeenCalledWith();
I've tried spying on both methods as well
const parentSpy = jest.spyOn(MyComponent.methods, 'submitAction');
const spy = jest.spyOn(MyComponent.methods, 'showToastMessage');
// ...
expect(spy).toHaveBeenCalled()
same results, test fail.
What am I missing?
Tech Stack: vue 3, jest, node 14

#TekkSparrow you can pass a heap of stuff into the shallowMount function. It accepts an object as a second argument which can look something like
import { shallowMount, createLocalVue } from '#vue/test-utils'
import Vuex from 'vuex'
const localVue = createLocalVue()
localVue.use(Vuex)
let mocks = {
// this could be something like the below examples
// I had in a previous project
$route: {
query: '',
path: '/some-path'
},
$router: [],
$validator: {
validateAll: jest.fn()
},
$toast: {
show: jest.fn(),
error: jest.fn()
},
}
let propsData = {
// some props you want to overwrite or test.
// needs to be called propsData
}
let methods = {
showToastMessage: jest.fn()
}
let store = new Vuex.Store({
actions: {
UPLOAD_ASSET: jest.fn(),
},
})
const wrapper = shallowMount(MyComponent, { mocks, propsData, methods, store, localVue })
I believe that by doing similar to the above, your mocked function will run and be recorded by the Jest spy.

Took me a minute to realize/try this, but looks like since my calling function is async that I was suppose to make my test async, and await the main method call. This seems to have done the trick. Here's what ended up being my solution:
test('the toast alert method is called', async () => {
let showToastMessage = jest.fn();
const spy = jest.spyOn(MyComponent.methods, 'showToastMessage');
const wrapper = shallowMount(MyComponent, { localVue });
const input = // some input data
await wrapper.vm.submitAction(input);
expect(spy).toHaveBeenCalled();
};

Related

Vue3 / Vuex State is empty when dispatching action inside of lifecycle hook inside of test

We're using the composition API with Vue 3.
We have a Vuex store that, amongst other things, stores the currentUser.
The currentUser can be null or an object { id: 'user-uuid' }.
We're using Vue Test Utils, and they've documented how to use the store inside of tests when using the Composition API. We're using the store without an injection key, and so they document to do it like so:
import { createStore } from 'vuex'
const store = createStore({
// ...
})
const wrapper = mount(App, {
global: {
provide: {
store: store
},
},
})
I have a component and before it is mounted I want to check if I have an access token and no user currently in the store.
If this is the case, we want to fetch the current user (which is an action).
This looks like so:
setup() {
const tokenService = new TokenService();
const store = useStore();
onBeforeMount(async () => {
if (tokenService.getAccessToken() && !store.state.currentUser) {
await store.dispatch(FETCH_CURRENT_USER);
console.log('User: ', store.state.currentUser);
}
});
}
I then have a test for this that looks like this:
it('should fetch the current user if there is an access token and user does not exist', async () => {
localStorage.setItem('access_token', 'le-token');
await shallowMount(App, {
global: {
provide: {
store
}
}
});
expect(store.state.currentUser).toStrictEqual({ id: 'user-uuid' });
});
The test fails, but interestingly, the console log of the currentUser in state is not empty:
console.log src/App.vue:27
User: { id: 'user-uuid' }
Error: expect(received).toStrictEqual(expected) // deep equality
Expected: {"id": "user-uuid"} Received: null
Despite the test failure, this works in the browser correctly.
Interestingly, if I extract the logic to a method on the component and then call that from within the onBeforeMount hook and use the method in my test, it passes:
setup() {
const tokenService = new TokenService();
const store = useStore();
const rehydrateUserState = async () => {
if (tokenService.getAccessToken() && !store.state.currentUser) {
await store.dispatch(FETCH_CURRENT_USER);
console.log('User: ', store.state.currentUser);
}
};
onBeforeMount(async () => {
await rehydrateUserState();
});
return {
rehydrateUserState
};
}
it('should fetch the current user if there is an access token and user does not exist', async () => {
localStorage.setItem('access_token', 'le-token');
await cmp.vm.rehydrateUserState();
expect(store.state.currentUser).toStrictEqual({ id: 'user-uuid' });
});
Any ideas on why this works when extracted to a method but not when inlined into the onBeforeMount hook?

how to test global event bus which is inside mounted method in jest

export default class Test extends Vue {
mounted() {
this.getData();
eventBus.$on("get", (id: string) => {
this.displayData(id);
});
}
getData(){
return "hello"
}
displayData(id){
}
I have written spec for the Test component like below. I have used Global Event Bus and trying to check event is emitted or not.
const EventBus = new Vue();
const GlobalPlugins = {
install(v:any) {
// Event bus
v.prototype.eventBus = EventBus;
}
};
const localVue = createLocalVue()
localVue.prototype.eventBus = createLocalVue();
localVue.use(GlobalPlugins);
describe('Test TestSuite', () => {
let wrapper: any
let TestObj: any;
beforeEach(() => {
const mocks = {
eventBus: {
$on: jest.fn(),
$emit: jest.fn()
}
};
wrapper = mount(Test, {
mocks,
localVue,
router
});
console.log(eventBus.$emit) ---> returns null
TestObj = wrapper.findComponent(Test).vm;
console.log(eventBus.$emit) ---> returns null
});
Here, I tried to check the get event is emitted or not. but it gives null object only.
Problem solved by emitting event and mocking components before mounting.

How to set mock nuxt asyncData in jest

I am using Nuxt.js and want to test my page which uses asyncData with Jest. I have a factory function to set up my wrapper, but it basically returns a shallowMount.
Expected
When clicking a button I want the function to behave differently depending on the query parameter. When running the test I want to mock this by setting it directly when creating the wrapper (Similar to setting propsData). E.g. const wrapper = factory({ propsData: { myQueryParam: 'some-value' } });
Result
However trying to set propsData still returns undefined: console.log(wrapper.vm.myQueryParam); // undefined while I would expect it to be 'some-value'
Question
Is there a different approach on how I can test this function that relies on query parameters?
Because asyncData is called before Vue is initialised, it means shallowMount doesn't work right out of the box.
Example:
page:
<template>
<div>Your template.</div>
</template>
<script>
export default {
data() {
return {}
},
async asyncData({
params,
error,
$axios
}) {
await $axios.get("something")
}
}
</script>
test:
import { shallowMount } from "#vue/test-utils";
describe('NewsletterConfirm', () => {
const axiosGetMock = jest.fn()
const axiosPostMock = jest.fn()
var getInitialised = async function (thumbprint) {
if (thumbprint == undefined) throw "thumbprint not provided"
let NewsletterConfirm = require('./_thumbprint').default
if (!NewsletterConfirm.asyncData) {
return shallowMount(NewsletterConfirm);
}
let originalData = {}
if (NewsletterConfirm.data != null) {
originalData = NewsletterConfirm.data()
}
const asyncData = await NewsletterConfirm.asyncData({
params: {
thumbprint
},
error: jest.fn(),
$axios: {
get: axiosGetMock,
post: axiosPostMock
}
})
NewsletterConfirm.data = function () {
return {
...originalData,
...asyncData
}
}
return shallowMount(NewsletterConfirm)
}
it('calls axios', async () => {
let result = await getInitialised("thumbprint")
expect(axiosGetMock).toHaveBeenCalledTimes(1)
});
});
Credits to VladDubrovskis for his comment: in this nuxt issue

How do I test a watcher on a tool that was imported in my component?

I use an instance of a class as a tool in one of my components. This component watches for changes in the class instance. However I fail at writing a test for that watcher.
I tried using jest.fn, spyOn and a setData, but none of these worked.
The class looks like this:
export default class myTool {
constructor () {
this._myData = null
}
get myData () {
return this._myData
}
set myData (updatedMyData) {
this._myData = updatedMyData
}
}
And the component:
import myTool from '#/utils/myTool'
export default {
...
data() {
return {
myTool: null
}
},
methods: {
handleMyDataUpdate(updatedMyData) {
// do something
}
},
mounted() {
this.$watch('myTool.myData', (updatedMyData) => {
this.handleMyDataUpdate(updatedMyData)
})
this.myTool = new myTool()
}
...
}
1st attempt with jest.fn:
test:
it('should call handleMyDataUpdate on myData update.', () => {
const wrapper = mountComponent()
const handleMyDataUpdate = jest.fn()
wrapper.setMethods({ handleMyDataUpdate })
wrapper.vm.myTool.myData = 5
expect(handleMyDataUpdate).toBeCalled()
})
2nd attempt with spyOn:
test:
it('should call handleMyDataUpdate on myData update.', () => {
const wrapper = mountComponent()
const spy = jest.spyOn(wrapper.vm, 'handleMyDataUpdate')
wrapper.vm.myTool.myData = 5
expect(spy).toBeCalled();
}
3rd attempt with setData:
test:
it('should call handleMyDataUpdate on myData update.', () => {
const wrapper = mountComponent()
const handleMyDataUpdate = jest.fn()
wrapper.setMethods({ handleMyDataUpdate })
wrapper.setData({
myTool: {
myData: 5
}
})
expect(handleMyDataUpdate).toBeCalled()
}
Result: the 3 things I tried always fail with the following reason: Expected mock function to have been called., whether I comment the line where myData is updated or not.
Other things that I tried:
I tried wrapping the expect line within a $nextTick, but it doesn't work either:
wrapper.vm.$nextTick(() => {
// expectation
done()
})
The following error outputs and the test is always considered as "passed", whereas it should be "failed":
console.error node_modules/vue/dist/vue.runtime.common.js:1739
{ Error: expect(jest.fn()).toBeCalled()
Looking at line 1739 of vue.runtime.common.js didn't help.
So how do I do to test my watcher?
The issue is your _myData in the myTool class is initially undefined, so it's not reactive. To resolve the issue, initialize _myData in myTool's constructor:
class myTool {
constructor() {
this._myData = null
}
// ...
}
Then, your "1st attempt" test should pass successfully.
demo

In Vuex + Jest, how to unit test a getter which is calling the store?

I'm trying to test the following very simple getter from my vuex store. It is simply concatenating two strings :
const getters = {
adressToGet: state => {
return state.baseAdress + store.getters.queryToGet
}
}
Mocking the state part is easy but I can't find a good way to mock the store.
If this was in a component, I could mount the component with mount or shallow and assign to it the mock store, but it isn't. This is from the vuex store.
This is my test code :
import Search from '#/store/modules/search'
jest.mock('#/store/modules/search.js')
describe('search.js', () => {
test('The adress getter gets the right adress', () => {
const state = {
baseAdress: 'http://foobar.com/'
}
// I define store here, but how can I inject it into my tested getter ?
const store = {
getters: {
queryToGet: 'barfoo'
}
}
expect(Search.getters.adressToGet(state)).toBe('http://foobar.com/barfoo')
})
})
I get http://foobar.com/undefined instead of expected.
What would be the best way to do this ?
Edit: Following the first comment, my new version, but it still gives the same result:
import Search from '#/store/modules/search'
import { createLocalVue } from '#vue/test-utils'
import Vuex from 'vuex'
jest.mock('#/store/modules/search.js')
describe('search.js', () => {
test('The adress getter gets the right adress', () => {
const localVue = createLocalVue()
localVue.use(Vuex)
const mockState = {
baseAdress: 'http://foobar.com/'
}
const store = new Vuex.Store({
state: mockState,
getters: {
queryToGet: function () {
return 'barfoo'
}
}
})
expect(Search.getters.adressToGet(mockState))
.toBe('http://foobar.com/barfoo')
})
})
After much research, I realized I had to mock the store dependency with Jest. This seems the correct way to do it and pass the test:
import Search from '#/store/modules/search'
jest.mock('#/store/index.js', () =>({
getters: {
queryToGet: 'barfoo'
}
}))
jest.mock('#/store/modules/search.js')
describe('search.js', () => {
test('The adress getter gets the right adress', () => {
const state = {
baseAdress: 'http://foobar.com/'
}
expect(Search.getters.adressToGet(state))
.toBe('http://foobar.com/barfoo')
})
})