4

I'm writing unit tests using vitest on a VueJS application.

As part of our application, we have a collection of API wrapper services, e.g. users.js which wraps our relevant API calls to retrieve user information:

import client from './client'

const getUsers = () => {
   return client.get(...)
}

export default {
   getUsers
}

Each of these services utilise a common client.js which in turn uses axios to do the REST calls & interceptor management.

For our units tests, I want to check that the relevant url is called, so want to spy on, or mock, client.

I have followed various examples and posts, but struggling to work out how I mock an import (client) of an import (users.js).

The closest I've been able to get (based on these posts - 1, 2) is:

import { expect, vi } from 'vitest'
import * as client from '<path/to/client.js>'
import UsersAPI from '<path/to/users.js>'

describe('Users API', () => {
    beforeEach(() => {
        const spy = vi.spyOn(client, 'default')    // mock a named export
        expect(spy).toHaveBeenCalled() // client is called at the top of users.js
    })

    test('Users API.getUsers', () => {
        UsersAPI.getUsers()
        expect(spy).toHaveBeenCalled()
    })
})

but it's tripping on:

 ❯ async frontend/src/api/client.js:3:31
      2| import store from '@/store'
      3| 
      4| const client = axios.create({
       |                              ^
      5|     headers: {
      6|         'Content-Type': 'application/json'

where it's still trying to load the real client.js file.

I can't seem to mock client explicitly because the import statements run first, and so client is imported inside users.js before I can modify/intercept it. My attempt at the mocking was as follows (placed between the imports and the describe):

vi.mock('client', () => {
    return {
        default: {
            get: vi.fn()
        }
    }
})
Joe Pavitt
  • 75
  • 1
  • 1
  • 6

3 Answers3

9

Mocking a module

vi.mock()'s path argument needs to resolve to the same file that the module under test is using. If users.js imports <root>/src/client.js, vi.mock()'s path argument needs to match:

// users.js
import client from './client' // => resolves to path/to/client.js
// users.spec.js
vi.mock('../../client.js')    // => resolves to path/to/client.js

It often helps to use path aliases here.

Spying/mocking a function

To spy on or mock a function of the mocked module, do the following in test():

  1. Dynamically import the module, which gets the mocked module.
  2. Mock the function off of the mocked module reference, optionally returning a mock value. Since client.get() returns axios.get(), which returns a Promise, it makes sense to use mockResolvedValue() to mock the returned data.
// users.spec.js
import { describe, test, expect, vi } from 'vitest'
import UsersAPI from '@/users.js'

vi.mock('@/client')

describe('Users API', () => {
  test('Users API.getUsers', async () => {
    1️⃣
    const client = await import('@/client')

    2️⃣
    const response = { data: [{ id: 1, name: 'john doe' }] }
    client.default.get = vi.fn().mockResolvedValue(response)
    
    const users = await UsersAPI.getUsers()
    expect(client.default.get).toHaveBeenCalled()
    expect(users).toEqual(response)
  })
})

demo

tony19
  • 125,647
  • 18
  • 229
  • 307
  • Thanks so much for such a rich response. I have tried to replicate in our own repository, but still get the same errors. I have modified the StackBlitz to reflect the same folder structure we have: https://stackblitz.com/edit/vue-vitest-mock-module-function-gt3gpq?file=package.json I don't hit the same error as shown in the stackblitz, we are able to run our test, but we still hit the same issue where it's trying to load the full `client.js` file. – Joe Pavitt May 18 '22 at 08:17
  • I've also noticed that in your example, removing `vi.mock('@/client')` doesn't change any behaviour, and the test still passes? Why would this be? It seems like our issues are stemming from our `client.js` importing a local `vuex` store, which then runs more imports, and so on. My understanding of the `vi.mock` is that it should stub the `client.js` file and prevent it from being loaded? Because the `import UsersAPI..` runs prior to the vi.mock line, I'm assuming this is why the `vi.mock` is not affecting any behaviour here? – Joe Pavitt May 18 '22 at 08:41
  • I've added another demo [here](https://stackblitz.com/edit/vue-vitest-mock-module-function-nrqerr?file=src/client.js), where it loads a `store/index.js` that I want to prevent loading. – Joe Pavitt May 18 '22 at 08:48
  • tvm, faced issue on mocking `axios.get.mockResolvedValueOnce` but that `axios.get = vi.fn().mockResolvedValueOnce` fixed it. – josevoid Jul 07 '22 at 02:10
5

Late to the party but just in case anyone else is facing the same issue.

I solved it by importing the module dependency in the test file and mocking the whole module first, then just the methods I needed.

import { client } from 'client';

vi.mock('client', () => {
    const client = vi.fn();
    client.get = vi.fn();
    
    return { client }
});

Then in those tests calling client.get() behind the scenes as a dependency, just add

  client.get.mockResolvedValue({fakeResponse: []});

and the mocked function will be called instead of the real implementation.

If you are using a default export, look at the vitest docs since you need to provide a default key.

If mocking a module with a default export, you'll need to provide a default key within the returned factory function object. This is an ES modules specific caveat, therefore jest documentation may differ as jest uses commonJS modules.

Paranoid Android
  • 4,672
  • 11
  • 54
  • 73
0

I've accepted the above answer, as that did address my initial question, but also wanted to include this additional step I required.

In my use case, I need to mock an entire module import, as I had a cascading set of imports on API files that in turn, imported more and more dependencies themselves.

To cut this, I found this in the vuex documentation about mocking actions:

https://vuex.vuejs.org/guide/testing.html#testing-actions

which details the use of webpack and inject-loader to substitute an entire module with a mock, preventing the source file loading at all.

Joe Pavitt
  • 75
  • 1
  • 1
  • 6