Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composables are not being mocked in Vitest #2066

Open
jaysaurus opened this issue Apr 21, 2023 · 10 comments
Open

Composables are not being mocked in Vitest #2066

jaysaurus opened this issue Apr 21, 2023 · 10 comments

Comments

@jaysaurus
Copy link

Subject of the issue

I have a vue 3 project using <script setup> tags

I import a composable into a component on the project.

I attempt to mock the composable in my test.

I observe that the composable is mocked accordingly during debugs of the test however once the debugger jumps to <script setup> the composable is observed and is not mocked

Steps to reproduce

  1. create a vue 3 project and add a component that has a <script setup> tag.
  2. create a composable that returns a handful of variables:
// fooComposable.js
export function useFooComposable () {
...
  return {
    bahComputedProperty, // contrive a computed property that simply returns false
    bazMethod, // contrive a method 
  }
}
  1. import the composable above into the component from step 1 and destructure the properties from the call and wire them up to a simple event...
import { useFooComposable }  from '~/composables/fooComposable.js'

const { bahComputedProperty, bazMethod }  = useFooComposable()

const myEvent = () => {
  if (bahComputedProperty) bazMethod()
}
<template>
  <button @click="myEvent">Click</button>
</template>
  1. create a spec for the above component and mock the composable with something to the effect of the below:

(note I have tried numerous different variations on this them, with and without vi.fn() )

import ...

import { useFooComposable }  from '@/composables/fooComposable.js' 

vi.mock( '@/composables/fooComposable.js', () => ({
  useFooComposable: vi.fn().mockReturnValue({ // I've tried this as a pojo closure as well, no difference
    bahComputedProperty: computed(() => return true), // i've tried this as a computed, a primitive, a vi.fn and a pojo getter, no difference
    bazMethod: vi.fn(), // i've tried this as a pojo method, no difference.
  }),
}))
  1. write a vi test that mounts the component, grabs bazMethod from useFooComposable, clicks the button and thereafter: expect(bazMethod).toBeCalled()

Expected behaviour

because the useFooComposable mock has switched bahComputedProperty to true, the test should fire bazMethod.

Actual behaviour

the debugger observes that both bahComputedProperty and bazMethod are mocked in the test script but when the debugger jumps to the myEvent call in the component, neither the property nor the event have been mocked. This isn't an issue for the contrived event here but handling all permutations of composables makes for extremely fragile tests in production code.

Possible Solution

I've tried setting it as a mock and I've tried directly altering wrapper.vm.bazMethod after the fact and neither gives me any change.

my colleague has had some success with

import * as bahComposable from ...

vi.spyon(bahComposable, 'useComposable').mockImplementation(() => { ... })

but this feels obscure given how much demand there is for composables in Vue 3. It simply feels - in the first place - like VTU isn't honouring vi.mock and that it probably should since this is the preferred manner of mocking imports in the given framework.

@maartentibau
Copy link

maartentibau commented Sep 5, 2023

I'm also running into problems with testing composables.

Example:

use-foo.ts

export function useFoo() {
  function bar(text: string): void {
    baz(text);
  }
  
  function baz(text: string): void {
    console.log(text);
  }
  
  return {
    bar,
    baz
  }
}

use-foo.spec.ts

describe('bar', () => {
  test(`should call baz with 'foobar'`, () => {
    // given
    const _useFoo = useFoo();
    vi.spyOn(_useFoo, 'baz');

    // when
    _useFoo.bar('foobar');

    // then
    expect(_useFoo.baz).toHaveBeenCalledWith('foobar')
  });
});

Stackblitz: https://stackblitz.com/edit/vitejs-vite-hlmyzy?file=src%2Fcomposables%2F__tests__%2Fuse-foo.spec.ts

I've tried multiple variations, the only thing that works if I fully mock the composable file, which personally does not feel right. A solution could be to make the methods return the value. But I feel that due to the fact that composables are such a big thing in Vue 3 (like @jaysaurus also mentions) it should be easy to, mock, stub, spy on them.

@LaurenceNairne
Copy link

@jaysaurus and @maartentibau, have you had any luck with this? I believe I'm having the same problem.

@felixzapata
Copy link

felixzapata commented Oct 2, 2023

I don't know yet why but changing vtu version from 2.4.0 to 2.4.1 and Vitest from 0.31.0 to any greater, breaks many tests I have with stores and composables mocked. If I keep 2.4.0 and 0.31.0 together, everything is fine.

And I don't know if it is because something has changed about how Vitest manages the mocks or other things related to vtu.

@felixzapata
Copy link

Finally, I realized that the problem (in some of my tests) was using properties from a composable that is mocked totally. These properties were obtained from the composable using destructuring assignment syntax. In current versions of my package, this error is never shown

@LaurenceNairne
Copy link

LaurenceNairne commented Oct 5, 2023

In case it helps, I've managed to successfully mock composables like as per the below.

Versions:

  • vitest: 0.34.4
  • vue/test-utils: 2.4.1
const mock = vi.hoisted(() => {
  return {
    composable: vi.fn().mockReturnValue({
      methodWithinComposable: vi.fn(),
    }),
  };
});

vi.mock('path/to/composable', () => ({
  composable: mock.composable,
}));

I've also found that using the JavaScript Debugger in VS Code to run the tests, often unearths some underlying issues that prevent the mock from successfully being used in tests. Usually it's been due to some other superfluous data that hasn't been mocked, but should be - though I'm mostly looking at higher level components at/near the top of the tree.

@matsura
Copy link

matsura commented Nov 11, 2023

@LaurenceNairne Hi, could you maybe explain how this resolves the issue you demonstrated in your stackblitz? I have the same issue, and could not understand your resolution.

@onekiloparsec
Copy link

I had troubles doing it right for a long time too. Thanks to @LaurenceNairne I managed to mock one of my composable (can't say yet if it works everywhere 😅):

All my composables are located in a folder (and subfolders) called composables/ under src/ with a big index.js file. And thus there are all accesible from @/composables (@ being the alias to src).

My component under test is using <script setup lang="ts"> and calls useMyCustomHook() during setup.

With the following, I managed to have the right objects customObject and customArray injected into my component, with their mocked values.

vi.mock('@/composables', async (importOriginal) => {
  const actual = await importOriginal()
  return {
    ...actual,
    useMyCustomHook: vi.fn().mockReturnValue({
      customObject: { property1: true, property2: 'dummy' },
      customArray: []
    }),
  }
})

@zymotik
Copy link

zymotik commented Mar 18, 2024

This is working for me using vitest (you can switch this code to jest pretty easily):

const mockUseSearch = vi.hoisted(() => ({
  loading: false,
  search: vi.fn(),
  searchResults: []
}));

vi.mock("@/ui/composables/search", () => ({
  useSearch: () => {
    return {
      loading: computed(() => mockUseSearch.loading),
      search: mockUseSearch.search,
      searchResults: computed(() => mockUseSearch.searchResults)
    };
  }
}));

describe("SearchDialog", () => {
  it("should have search dialog", () => {
    render(SearchDialog);

    expect(screen.getByRole("textbox")).toBeInTheDocument();
    expect(screen.getByRole("button")).toBeInTheDocument();

    // you could also fire a click event on the button
    // and check the "search" spy if you wanted
    // fireEvent.click(screen.getByRole("button"));
    // expect(mockUseSearch.search).toHaveBeenCalled();
  });

  it("should show previous results when opened on another page", () => {
    mockUseSearch.searchQuery = "Hello world";
    mockUseSearch.searchResults = [{}, {}];

    render(SearchDialog);

    expect(screen.getByText(mockUseSearch.searchQuery)).toBeInTheDocument();
    expect(screen.getByRole("list")).toHaveLength(mockUseSearch.searchResults.length);
  });
});

@bdeo
Copy link

bdeo commented Mar 22, 2024

Any updates here? the above solutions in this thread are not working for me.

@zymotik
Copy link

zymotik commented Mar 25, 2024

Any updates here? the above solutions in this thread are not working for me.

Can you post an example of your code @bdeo?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants