Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spying On/Mocking Import of an Import

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()
        }
    }
})
like image 908
Joe Pavitt Avatar asked Jun 05 '26 23:06

Joe Pavitt


2 Answers

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

like image 82
tony19 Avatar answered Jun 07 '26 11:06

tony19


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.

like image 45
Mirko Avatar answered Jun 07 '26 12:06

Mirko