Jest test on functions which are in onMounted Composition API Vue 2 - vue.js

Can anyone tell me how to write a test case for the functions which are in onMounted hook? I am using vue 2 with composition api plugin.
const getUsers = async () => {
const usersQuery = `
query {
users: {
id
username
}
}
`
try {
const result = await apolloClient.getGraphqlData(usersQuery)
if (result) users.value = result.data.users
} catch (err) {
console.log('Error while receiving users', err)
}
}
Below is my onMounted hook
onMounted(() => {
getUsers()
})

You can't mock local functions defined in setup(). The functions have to be exposed so that the unit tests can access them. One workaround is to declare the methods in an external file, adjacent to the component:
// mylib.js
export const getUsers = async () => { /*...*/ }
And import that file in your component:
import { onMounted } from '#vue/composition-api'
import { getUsers } from './mylib'
export default {
setup() {
onMounted(() => getUsers())
}
}
Then in your test file, import the same file, and use jest.mock() to auto-mock it, which would allow you to verify that the function was called upon the component mounting:
import { getUsers } from '#/components/mylib'
import MyComponent from '#/components/MyComponent.vue'
jest.mock('#/components/setupFns')
describe('MyComponent', () => {
beforeEach(() => jest.resetAllMocks())
it('calls getUsers() on mount', () => {
shallowMount(MyComponent)
expect(getUsers).toHaveBeenCalledTimes(1)
})
})
demo

Related

Vuex + Jest + Composition API: How to check if an action has been called

I am working on a project built on Vue3 and composition API and writing test cases.
The component I want to test is like below.
Home.vue
<template>
<div>
<Child #onChangeValue="onChangeValue" />
</div>
</template>
<script lang="ts>
...
const onChangeValue = (value: string) => {
store.dispatch("changeValueAction", {
value: value,
});
};
</scirpt>
Now I want to test if changeValueAction has been called.
Home.spec.ts
...
import { key, store } from '#/store';
describe("Test Home component", () => {
const wrapper = mount(Home, {
global: {
plugins: [[store, key]],
},
});
it("Test onChangeValue", () => {
const child = wrapper.findComponent(Child);
child.vm.$emit("onChangeValue", "Hello, world");
// I want to check changeValueAction has been called.
expect(wrapper.vm.store.state.moduleA.value).toBe("Hello, world");
});
});
I can confirm the state has actually been updated successfully in the test case above but I am wondering how I can mock action and check if it has been called.
How can I do it?
I have sort of a similar setup.
I don't want to test the actual store just that the method within the component is calling dispatch with a certain value.
This is what I've done.
favorite.spec.ts
import {key} from '#/store';
let storeMock: any;
beforeEach(async () => {
storeMock = createStore({});
});
test(`Should remove favorite`, async () => {
const wrapper = mount(Component, {
propsData: {
item: mockItemObj
},
global: {
plugins: [[storeMock, key]],
}
});
const spyDispatch = jest.spyOn(storeMock, 'dispatch').mockImplementation();
await wrapper.find('.remove-favorite-item').trigger('click');
expect(spyDispatch).toHaveBeenCalledTimes(1);
expect(spyDispatch).toHaveBeenCalledWith("favoritesState/deleteFavorite", favoriteId);
});
This is the Component method:
setup(props) {
const store = useStore();
function removeFavorite() {
store.dispatch("favoritesState/deleteFavorite", favoriteId);
}
return {
removeFavorite
}
}
Hope this will help you further :)

Mock of store action of vuex does not mocked

I have small vue component that on created hook dispatch some action
#Component
export default class SomeComponent extends Vue {
created() {
store.dispatch('module/myAction', { root: true });
}
}
and I wrote next test
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueRouter);
const localRouter = new VueRouter();
describe('SomeComponent.vue Test', () => {
let store: any;
beforeEach(() => {
store = new Vuex.Store({
modules: {
module: {
namespaced: true,
actions: {
myAction: jest.fn()
}
}
}
});
});
it('is component created', () => {
const wrapper = shallowMount(SomeComponent, {
localVue,
store,
propsData: {}
});
expect(wrapper.isVueInstance()).toBeTruthy();
});
});
but for some reason the "real" code are executed and I got a warning
isVueInstance() is deprecated. In your test you should mock $store object and it's dispatch function. I fixed typo in created(), here's my version of SomeComponent and working test, hope that would help.
#Component
export default class SomeComponent extends Vue {
created () {
this.$store.dispatch('module/myAction', { root: true })
}
}
import { shallowMount, Wrapper } from '#vue/test-utils'
import SomeComponent from '#/components/SomeComponent/SomeComponent.vue'
let wrapper: Wrapper<SomeComponent & { [key: string]: any }>
describe('SomeComponent.vue Test', () => {
beforeEach(() => {
wrapper = shallowMount(SomeComponent, {
mocks: {
$store: {
dispatch: jest.fn()
}
}
})
})
it('is component created', () => {
expect(wrapper.vm.$store.dispatch).toBeCalled()
expect(wrapper.vm.$store.dispatch).toBeCalledWith('module/myAction', { root: true })
})
})
Also keep in mind that when you test SomeComponent or any other component you should not test store functionality, you should just test, that certain actions/mutations were called with certain arguments. The store should be tested separately. Therefore you don't need to create real Vuex Store when you test components, you just need to mock $store object.

$router.push not triggering in unit testing

I have the following problem while trying to unit test my Vue application.
Even spying and mocking $router.push, I still can't make it to be called while inside unit testing:
This is my unit testing (Home.spec.js)
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueRouter);
describe('Home.vue', () => {
let actions;
let getters;
let store;
let router;
beforeEach(() => {
actions = {
[FETCH_USER_REPOSITORIES]: jest.fn()
};
getters = {
getTopThreeRepositories: jest.fn(repositoryMock.getRepositories)
};
store = new Vuex.Store({ getters, actions });
router = new VueRouter();
});
it('should redirect when 404 status code received', async () => {
jest.spyOn(store, 'dispatch').mockRejectedValue({ statusCode: 404 });
jest.spyOn(router, 'push').mockResolvedValue({});
const wrapper = await shallowMount(Home, {
store,
localVue,
router
});
expect(router.push).toHaveBeenCalledWith('/not-found');
});
});
Now, this is my Home.vue:
import { mapGetters } from 'vuex';
import { FETCH_USER_REPOSITORIES } from "../store/actions";
import RepositoryList from "#/components/RepositoryList";
import Card from "#/components/Card";
export default {
name: 'view-home',
components: {
Card,
RepositoryList
},
async beforeMount() {
try {
await this.$store.dispatch(FETCH_USER_REPOSITORIES, 'some-repo');
} catch(err) {
console.log(this.$router);
await this.$router.push('/not-found');
}
},
computed: {
...mapGetters(["getTopThreeRepositories"])
}
}
The console log shows the $router correctly, with the spy.
If I force the calling inside the unit testing, it works, but the expects always fails giving me back that $router.push hasn't been called.
Can anyone help me, please?
Thanks!
You should specify $store and $route as mocks in your mounting options, as shown below. Also there's no need to await shallowMount because shallowMount does not return a Promise, so the await would just return immediately.
describe('Home.vue', () => {
it('should redirect when 404 status code received', () => {
const $store = {
dispatch: jest.fn()
}
const $router = {
push: jest.fn()
}
const wrapper = shallowMount(Home, {
localVue,
mocks: {
$store,
$router,
}
});
expect($router.push).toHaveBeenCalledWith('/not-found');
})
})

Vue composable scoping issue

We're using Vue 2 with the Vue Composition API and we're trying to create a composable that will expose application preferences:
// useApplicationPreferences.ts
import { ref, watch } from '#vue/composition-api'
import { useSetDarkModeMutation, useViewerQuery } from 'src/graphql/generated/operations'
const darkMode = ref(false) // global scope
export const useApplicationPreferences = () => {
const { mutate: darkModeMutation } = useSetDarkModeMutation(() => ({
variables: {
darkMode: darkMode.value,
},
}))
watch(darkMode, async (newDarkMode) => {
console.log('darkMode: ', newDarkMode)
await darkModeMutation()
})
return { darkMode }
}
This code works fine but when the composable is used in two components that are rendered at the same time we can see that watch has been triggered twice. This is easily solved by moving the watch function to the global scope (outside the function).
However, the issue then is that we can't use the darkModeMutation. This graphql mutation can not be moved to the global scope outside of the function, if we do that the page doesn't even get rendered.
The goal is to have darkMode available in many places and when the value of the darkMode ref changes the mutation is only triggered once. How can this be achieved?
Solved the issue by creating a callable function that starts watch only when required (i.e. only once somewhere in the app).
// useApplicationPreferences.ts
import { ref, watch } from '#vue/composition-api'
import { useSetDarkModeMutation, useViewerQuery } from 'src/graphql/generated/operations'
const darkMode = ref(false) // global scope
export const useApplicationPreferences = () => {
const { mutate: darkModeMutation } = useSetDarkModeMutation(() => ({
variables: {
darkMode: darkMode.value,
},
}))
const startWatch = () => {
watch(darkMode, async (newDarkMode) => {
await darkModeMutation()
})
}
return { darkMode, startWatch }
}
Which the can be called once in MainLayout.vue:
// MainLayout.vue
import { defineComponent } from '#vue/composition-api'
import { useApplicationPreferences } from 'useApplicationPreferences'
export default defineComponent({
setup() {
const { startWatch } = useApplicationPreferences()
startWatch()
},
})
All other components can then simply consume (get/set) the darkMode ref as required while watch is only running once.
// Settings.vue
import { defineComponent } from '#vue/composition-api'
import { useApplicationPreferences } from 'useApplicationPreferences'
export default defineComponent({
setup() {
const { darkMode } = useApplicationPreferences()
return { darkMode }
},
})

How can I test actions within a Vuex module?

I want to test a vuex module called user.
Initially, I successfully registered my module to Vuex. Its works as expected.
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
user
}
})
export default store
My user module is defined as follows
store/modules/user.js
const state = {
token: getToken() || '',
}
export const getters = {
token: state => state.token,
}
const mutations = {
[SET_TOKEN]: (state, token) => {
state.token = token
}
}
const actions = {
[LOGIN] ({ commit }, body) {
return new Promise((resolve, reject) => {
login(body).then(response => { //login is an api method, I'm using axios to call it.
const { token } = response.data
setToken(token)
commit(SET_TOKEN, token)
resolve()
}).catch(error => {
reject(error)
})
})
}
}
export default {
state,
getters,
mutations,
actions
}
login api
api/auth.js
import request from '#/utils/request'
export function login (data) {
return request({
url: '/auth/login',
method: 'post',
data
})
}
axios request file
utils/request
import axios from 'axios'
import store from '#/store'
import { getToken } from '#/utils/auth'
const request = axios.create({
baseURL: process.env.VUE_APP_BASE_API_URL,
timeout: 5000
})
request.interceptors.request.use(
config => {
const token = getToken()
if (token) {
config.headers['Authentication'] = token
}
return config
}
)
export default request
When I want to write some test (using Jest), for example login action as shown above.
// user.spec.js
import { createLocalVue } from '#vue/test-utils'
import Vuex from 'vuex'
import actions from '#/store/modules/user'
const localVue = createLocalVue()
localVue.use(Vuex)
test('huhu', () => {
expect(true).toBe(true)
// implementation..
})
How can I write test for my Login action? Thanks. Sorry for my beginner question.
EDIT: SOLVED Thank you Raynhour for showing to me right direction :)
import { LOGIN } from '#/store/action.types'
import { SET_TOKEN } from '#/store/mutation.types'
import { actions } from '#/store/modules/user'
import flushPromises from 'flush-promises'
jest.mock('#/router')
jest.mock('#/api/auth.js', () => {
return {
login: jest.fn().mockResolvedValue({ data: { token: 'token' } })
}
})
describe('actions', () => {
test('login olduktan sonra tokeni başarıyla attı mı?', async () => {
const context = {
commit: jest.fn()
}
const body = {
login: 'login',
password: 'password'
}
actions[LOGIN](context, body)
await flushPromises()
expect(context.commit).toHaveBeenCalledWith(SET_TOKEN, 'token')
})
})
Store it's just a javascript file that will export an object. Not need to use vue test util.
import actions from '../actions'
import flushPromises from 'flush-promises'
jest.mock('../api/auth.js', () => {
return {
login: jest.fn()..mockResolvedValue('token')
}; // mocking API.
describe('actions', () => {
test('login should set token', async () => {
const context = {
commit: jest.fn()
}
const body = {
login: 'login',
password: 'password'
}
actions.login(context, body)
await flushPromises() // Flush all pending resolved promise handlers
expect(context.commit).toHaveBeenCalledWith('set_token', 'token')
})
})
but you need to remember that in unit tests all asynchronous requests must be mocked(with jest.mock or something else)