Vue test utils: mock getter return value - vue.js

I'm writing a test that mocks the return value of a namespaced getter, which accepts a single argument. The test parameter will be the mocked return value for the getter, which controls the visibility of an element on the page.
Mocking the getter with a generic return value allows it to be defined, but so far mocking the return value with the test parameter is not working.
Getter (in the "app" module):
const getters = {
myGetter:
(state) =>
(getterArg) => {
return state.someStateVariable === true
? valueForTrue
: valueForFalse;
}
};
Test:
import { cloneDeep } from "lodash";
import { createLocalVue, shallowMount } from "#vue/test-utils";
import MyComponent from "#/views/MyComponent.vue";
import { storeConfig } from "#/store";
import Vuex from "vuex";
describe("My test", () => {
let localVue;
let store;
let wrapper;
beforeEach(() => {
localVue = createLocalVue();
localVue.use(Vuex);
});
test.each([[false], [true]])(
"Test param: '%s'",
(testParam) => {
store = new Vuex.Store({
modules: {
app: {
namespaced: true,
...cloneDeep(storeConfig.modules.app),
state: {
...cloneDeep(storeConfig.modules.app.state),
getters: {
...cloneDeep(storeConfig.modules.app.getters),
// getter defined, but doesn't include test param
myGetter: jest.fn().mockReturnValue(jest.fn())
// getter variation with a mocked return value (test fails due to testParam not being returned)
myGetter: jest.fn().mockReturnValue(jest.fn().mockReturnValue(testParam))
// getter variation with the argument passed by the component, and a mocked return value (test fails due to testParam not being returned)
myGetter: jest.fn("getterArg").mockReturnValue(jest.fn().mockReturnValue(testParam))
}
}
}
});
wrapper = shallowMount(MyComponent, {
localVue,
store
});
expect(wrapper.find("#my-element").exists()).toBe(
testParam
);
I also tried updating the wrapper after creation:
// Test fails due to getter being undefined
Object.defineProperty(wrapper.vm, "app/myGetter", {
value: () => {
return testParam;
}
});
// Test fails due to getter being undefined
Object.defineProperty(wrapper.vm, "app/myGetter", {
value: ("getterArg") => {
return testParam;
}
});
How can this namespaced getter be configured to dynamically return the test parameter?

Related

Vue test utils: extend VueWrapper to add plugin method

I am using Vue test utils and Typescript. I have added Data Test ID Plugin.
How can I extend VueWrapper interface to avoid this error:
Property 'findByTestId' does not exist on type 'VueWrapper<{ $: ComponentInternalInstance; $data: { showUserMenu: boolean ...
One solution is to export a type that adds findByTestId:
// my-vue-test-utils-plugin.ts
import { config, DOMWrapper, createWrapperError, type VueWrapper } from '#vue/test-utils'
👇
export type TestWrapper = VueWrapper<any> & {
findByTestId: (selector: string) => DOMWrapper<HTMLElement>
}
const DataTestIdPlugin = (wrapper: VueWrapper<any>) => {
function findByTestId(selector: string) {
const dataSelector = `[data-testid='${selector}']`
const element = wrapper.element.querySelector(dataSelector)
if (element) {
return new DOMWrapper(element)
}
return createWrapperError('DOMWrapper')
}
return {
findByTestId
}
}
config.plugins.VueWrapper.install(DataTestIdPlugin as any)
Then, use type assertion (as keyword followed by the exported type above) on the mount() result:
// MyComponent.spec.ts
import type { TestWrapper } from './my-vue-test-utils-plugin.ts'
describe('MyComponent', () => {
it('renders properly', () => { 👇
const wrapper = mount(MyComponent) as TestWrapper
expect(wrapper.findByTestId('my-component').text()).toBe('Hello World')
})
})
Another option is to create a .d.ts file e.g. vue-test-utils.d.ts with the following content:
import { DOMWrapper } from '#vue/test-utils';
declare module '#vue/test-utils' {
export class VueWrapper {
findByTestId(selector: string): DOMWrapper[];
}
}
So you're able to extend the existing definition of the VueWrapper class.

Passing mocked methods to mount/shallowMount using Vue Test Utils

Can somebody please explain to me why a mocked function passed in a methods object to shallowMount can not be accessed in the test through the wrapper object and instead must be accessed by first creating a variable as a reference to the mocked function?
I have tried mount and shallowMount, created/mounted hooks and also by calling the function directly as opposed to inside the created/mounted hook.
// TestComponent.spec.js
import TestComponent from '#/components/TestComponent'
import { shallowMount, createLocalVue } from '#vue/test-utils'
const localVue = createLocalVue()
const setLoadingMock = jest.fn() // mock function that is accessible in the test
function createWrapper () {
const defaultMountingOptions = {
localVue,
methods: {
setLoading: setLoadingMock
}
}
return shallowMount(TestComponent, defaultMountingOptions)
}
describe('TestComponent.vue', () => {
let wrapper
beforeEach(() => {
wrapper = createWrapper()
});
it('will call setLoading', () => {
expect(wrapper.vm.setLoading).toHaveBeenCalled()
// FAILS. Console message:
// Matcher error: received value must be a mock or spy function
// Received has type: function
// Received has value: [Function bound mockConstructor]
})
it('will call setLoading', () => {
expect(setLoadingMock).toHaveBeenCalled() // PASSES
})
})
TestComponent.vue
export default {
name: 'TestComponent',
mounted () {
this.setLoading()
},
methods: {
setLoading () {
console.log('Original method'); // Never logs
}
}
}
mount or shallowMount are not important in this case. mount means test will mount component and its child components, while shallowMount will mount only component and stub its child components.
You are mocking the setLoading method, which means that you are replacing the original method with a mock. Meaning, when setLoading method is called, it won't run the code from your component, but code from the test mock - in this case jest.fn().
Purpose of mocking is to check if the mocked method was called correctly.
Also, wrapper.vm.setLoading calls the setLoading method.
Instead of referencing the wrapper instance, you should spy the method, e.g.:
const setLoading = jest.spyOn(wrapper.vm, 'setLoading');
expect(setLoading).toHaveBeenCalled() ;

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')
})
})

Pass params to mapGetters

I use vuex and mapGetters helper in my component. I got this function:
getProductGroup(productIndex) {
return this.$store.getters['products/findProductGroup'](productIndex)
}
Is it possible to move this somehow to mapGetters? The problem is that I also pass an argument to the function, so I couldn't find a way to put this in mapGetters
If your getter takes in a parameter like this:
getters: {
foo(state) {
return (bar) => {
return bar;
}
}
}
Then you can map the getter directly:
computed: {
...mapGetters(['foo'])
}
And just pass in the parameter to this.foo:
mounted() {
console.log(this.foo('hello')); // logs "hello"
}
Sorry, I'm with #Golinmarq on this one.
For anyone looking for a solution to this where you don't need to execute your computed properties in your template you wont get it out of the box.
https://github.com/vuejs/vuex/blob/dev/src/helpers.js#L64
Here's a little snippet I've used to curry the mappedGetters with additional arguments. This presumes your getter returns a function that takes your additional arguments but you could quite easily retrofit it so the getter takes both the state and the additional arguments.
import Vue from "vue";
import Vuex, { mapGetters } from "vuex";
Vue.use(Vuex);
const store = new Vuex.Store({
modules: {
myModule: {
state: {
items: [],
},
actions: {
getItem: state => index => state.items[index]
}
},
}
});
const curryMapGetters = args => (namespace, getters) =>
Object.entries(mapGetters(namespace, getters)).reduce(
(acc, [getter, fn]) => ({
...acc,
[getter]: state =>
fn.call(state)(...(Array.isArray(args) ? args : [args]))
}),
{}
);
export default {
store,
name: 'example',
computed: {
...curryMapGetters(0)('myModule', ["getItem"])
}
};
Gist is here https://gist.github.com/stwilz/8bcba580cc5b927d7993cddb5dfb4cb1