5

I have a component and a Pinia store which contains a state and some actions. The code works perfectly fine in browser and in E2E (cypress) tests, but fail on unit tests. I'm using vue-testing-utils and vitest.

The store function can be called fine from the unit test when the button is clicked, but if the function is in the mounted or main script, it fails the test

src/components/UsersComponent.vue

<script setup>
import { onMounted } from 'vue'
import { useUsersStore } from '@/stores/users.store'

const usersStore = useUsersStore()
// usersStore.resetStatus() // <- This fails in the unit test

onMounted(() => {
  usersStore.resetStatus() // <- This fails in the unit test
})

function changeStatus() {
  usersStore.changeStatus() // <- This passes in the unit test
}
</script>

<template>
  <div>
    <p>Status: {{ usersStore.status }}</p>
    <button @click="changeStatus()">Change Status</button>
  </div>
</template>

src/stores/users.store.js

import { defineStore } from 'pinia'
import { usersAPI } from '@/gateways'

export const useUsersStore  = defineStore({
  id: 'users',
  persist: true,

  state: () => ({
    status: 'ready',
  }),

  getters: {},

  actions: {
    resetStatus() {
      this.status = 'ready'
    },
    changeStatus() {
      this.status = 'loading'
    },
  },
})

src/components/tests/UsersComponent.spec.js

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'

import UsersComponent from '@/components/UsersComponent.vue'
import { useUsersStore } from '@/stores/users.store'

const wrapper = mount(UsersComponent, {
  global: {
    plugins: [createTestingPinia({ createSpy: vi.fn() })],
  },
})
const usersStore = useUsersStore()

describe('UsersComponent', () => {
  it('store function is called', async () => {
    // arrange
    const spy = vi.spyOn(usersStore, 'resetStatus')
    const button = wrapper.find('button')

    // act
    await button.trigger('click')

    // assert
    expect(spy).toHaveBeenCalled()
  })
})

The unit tests return 2 different error. The first is a console log when the function tries to run in onMounted() and the second is what vitest returns.

stderr | unknown test
[Vue warn]: Unhandled error during execution of mounted hook 
  at <UsersComponent ref="VTU_COMPONENT" >
  at <VTUROOT>
 FAIL  src/components/__tests__/UsersComponent.spec.js [ src/components/__tests__/UsersComponent.spec.js ]
TypeError: usersStore.resetStatus is not a function
 ❯ src/components/UsersComponent.vue:16:14
     16|
     17| <template>
     18|   <div>
       |  ^
     19|     <p>Status: {{ usersStore.status }}</p>
     20|     <button @click="changeStatus()">Change Status</button>

I know this example is a little basic and doesn't really serve a purpose, but I'm wondering how I can have store functions inside the onMounted() (or similar places) without it breaking all my unit tests.

Josh Larminay
  • 63
  • 2
  • 5

2 Answers2

0

Maybe this can be useful to you:

describe('UsersComponent',  () => {
  it('changeStatus function is called', async () => {
    const wrapper = mount(UsersComponent, {
      mounted: vi.fn(), // With this you mock the onMounted
      global: {
        plugins: [createTestingPinia({
          initialState: { // Initialize the state
            users: { status: 'ready' }, 
          }
        })]
      }
    })  
  
    // Spy the method you call...
    const spy = vi.spyOn(wrapper.vm, 'changeStatus');

    wrapper.vm.changeStatus()

    expect(spy).toHaveBeenCalled()
    expect(spy).toHaveBeenCalledTimes(1)
  })
})
0

I use vitest, pinia and testing-library (based on vue-test-utils). IMHO, there is no need :

  • to mock onMounted : you're goal is not to check that onMounted is called => this is Vue internal test

  • to use vi.spyOn, by default, pinia mocks all methods. You could directly do this in your test :

    const usersStore = useUsersStore();

    expect(usersStore.resetStatus).toHaveBeenCalledTimes(1);

I do this for all onBeforeMount, onMounted that calls http calls from the store

onMounted(async() => {
  await Promise.all([
    valuesStore.getAllValues(),
    serviceRequestStore.getServiceRequest(),
  ]);
});

And in the test

it('gets the service request from the serviceRequest store', () => {
  renderTheComponent();

  const serviceRequestStore = useServiceRequestStore();
  expect(serviceRequestStore.getServiceRequest).toHaveBeenCalledTimes(1);
});

I think this should work with vue-test-utils in the same way. All my component's hooks are async and return promise.

General Grievance
  • 4,555
  • 31
  • 31
  • 45