vuejs / test-utils

Vue Test Utils for Vue 3
https://test-utils.vuejs.org
MIT License
1.04k stars 244 forks source link

Feature request: global.mocks to also mock into the script setup, not just the template #2119

Open jrutila opened 1 year ago

jrutila commented 1 year ago

I am coding with Nuxt and would like to component test following component:

<script setup lang="ts">
import { ref } from 'vue'
const hello = ref('hello')

function clicked() {
  //@ts-ignore For example Nuxt will inject these kinds of global functions
  $fetch('http://www.example.com').then((response : any) => {
    hello.value = response.data
  })
}

</script>

<template>
  <button @click="clicked">{{ hello }}</button>
  <!-- this emulates components that use a global function like $t for i18n -->
  <!-- this function can be mocked using global.mocks -->
</template>

The problem is the $fetch call that is with Nuxt freely available to use in the component. I would like to mock the $fetch method.

The test would then be like this:

  it('mocks a global function used in a script setup', async () => {
    const wrapper = mount(ScriptSetupWithGlobalFunction, {
      global: {
        mocks: {
          $fetch: async (url) => ({ data: 'mocked' })
        }
      }
    })
    expect(wrapper.text()).toContain('hello')
    await wrapper.find('button').trigger('click')
    expect(wrapper.text()).toContain('mocked')
  })

The solution would be that when I mock the $fetch it will become available also in the script setup. At the moment it is only available to use in the template code.

jrutila commented 1 year ago

I already tested this and have a feature branch for this.

cexbrayat commented 1 year ago

Hi @jrutila

I'm not sure we need this, as Nuxt is bit special. Maybe you're looking for https://github.com/antfu/unplugin-auto-import (see https://github.com/vitest-dev/vitest/discussions/1565).

You can also check out nuxt-vitest which may help (from Daniel Roe who is one the Nuxt contributor)

cexbrayat commented 1 year ago

For example https://github.com/danielroe/nuxt-vitest#mocknuxtimport might be what you need

jrutila commented 1 year ago

Thanks for the hints for Nuxt. I will look into them.

On the other hand, including the mocked variables in the global doesn't really break anything, and if there are any other special frameworks the users could achieve the global mocking without needing external libraries. In the end, a unit test tool should be strong enough to let you do "stupid" things because you might be dependent on a library that is doing stupid things.

cexbrayat commented 1 year ago

I think that should already be the case, as mocks are supposed to be added to the global instance. But maybe there is an issue with script setup components, which are a bit special.

jrutila commented 1 year ago

Check out my pull request. There is a unit test case that shows how it is supposed to work. At least that haven't been working on my project or the test case. And there was no test case for this in the unit tests. Was it in the previous version of vue-test-utils (for Vue 2) where it worked?

jrutila commented 1 year ago

I'm not sure we need this, as Nuxt is bit special. Maybe you're looking for https://github.com/antfu/unplugin-auto-import (see vitest-dev/vitest#1565). For example https://github.com/danielroe/nuxt-vitest#mocknuxtimport might be what you need

I tried these and have to say they are quite complex to set up for my case as all I want to do is mock the global $fetch function for the component I test (I am running Cypress component tests). Feels like shooting a fly with a cannon. Using only vue/test-utils would be the perfect solution here. I really hope you accept my PR.

I will, for now, build my own fork with the feature so that I get forward. Maybe someday Cypress even supports out-of-the-box Nuxt component testing.

jrutila commented 1 year ago

Okay, apparently vue/test-utils is bundled inside cypress, so it is not that easy to just build my own version. But! I found a workaround: a plugin. I can write the following plugin:

Cypress.Commands.add("mount", (MountedComponent, options) => {
  const fetchMethod = async (_url) => ({
    data: "mocked",
  })

  const fetchPlugin = {
    install(_app, _options) {
      globalThis.$fetch = fetchMethod
    },
  }
 options.global.plugins.push(fetchPlugin)
 return mount(MountedComponent, options)
})

Now the $fetch method is globally available for the component. I just leave this here if someone finds this later. Still, supporting the mocking would be good. I even update the docs accordingly in the PR.

freakzlike commented 1 year ago

Hi @jrutila, Thanks for the PR! But I don't feel comfortable adding this to VTU by default as this is nothing Vue does. Although isn't it possible to set globalThis before mount which is more intended by the user?

jrutila commented 1 year ago

Yeah, and you can use a plugin to achieve this same. I updated the docs regarding this with the snippet below. So, if the global.mocks will add it to this in the case of non-setup, why is it different from adding it to globalThis in the case of setup?

<template>
  <button @click="onClick" />
</template>

<script>
export default {
  methods: {
    onClick() {
      this.$store.dispatch('click')
    }
  }
}
</script>

// Or if you are using script setup

<script setup>
function onClick() {
  $store.dispatch('click')
}
</script>
lmiller1990 commented 1 year ago

Still going over the thread and understanding the use case. The concern I have is Nuxt and Test Utils would be injecting $fetch in a different fashion - so we can't really guarantee your test actually matches with your production code.

That said -- I am surprised this doesn't already work. I thought you could mock Vuex using:

mocks: {
  $store: /* mock store */
}

which would be assigned to this. I really thought we had this in Test Utils v1 - can someone verify this?

Edit: yes this works: https://github.com/lmiller1990/vue-testing-handbook/blob/master/demo-app-vue-3/tests/unit/ComponentWithButtons.spec.js#L51-L66. I understand the issue better now - it's not about mocking this.$fetch, but globalThis.$fetch. I agree that we might not want to overload mocks.

One option would be a new API, like

mount(Comp, {
  globalThis: ...
})

But isn't this already supported by test runners like Jest / Vitest?


If you are using Cypress, why not just use cy.intercept? You can use the real request then, and Cypress can intercept and modify it at the network level. No mocking needed - and your test will be much more resilient, since you are mocking less. I think you'd need to configure vite.config with previously mentioned auto-import library to ensure $fetch is available.


It'd sure be nice to get first class Nuxt support in Cypress. We talked about it a lot here https://github.com/cypress-io/cypress/issues/23619. Maybe you saw this already? It's complex, since Nuxt does a lot of things under the hood, we need to hook into the process, which isn't exactly a public API. Also, there is the whole SSR element to consider - we don't really have a good way to spin up the Nuxt server and manage all that.

I work at Cypress, and on Test Utils, I am well positioned to help solve this issue either here, there, or in some other fashion. I'm curious why cy.intercept isn't a valid option here - it's vastly more ideal to mock a network request than $fetch, imo. I get the whole "using a cannon to kill a fly" aspect - Cypress is big and complex tool - but over the years of dealing with Test Utils, I'm starting to think jsdom + Test Utils is more like bringing a sword to a gun fight - using a browser to test code intended run in a browser is closer to production, which is ideal (be it Cypress, Playwright - not an evangelist, not trying to sell you on any one tool).

I will, for now, build my own fork with the feature so that I get forward. Maybe someday Cypress even supports out-of-the-box Nuxt component testing.

If you do want to contribute to cypress/vue, happy to help out with development workflow. You'd likely just clone the Cypress repo, symlink to your local version of Test Utils, and do cd npm/vue && yarn watch, and use Cypress to develop your feature. Ping me if you do want to work on Cypress / Vue or even Nuxt integration, I'd love to see this.

lmiller1990 commented 1 year ago

Happy to leave this as feature request for now - I'd like to see it addressed more broadly, probably "how best to handle auto injected global variables" or something -- since this is a general problem to solve with the auto import stuff that's becoming popular (personally not a fan - it makes it really hard to onboard people and IDE support is kind of meh, I much prefer just importing the code 🤷 )

jrutila commented 1 year ago

If you are using Cypress, why not just use cy.intercept?

I would if I would be doing e2e testing with Cypress. But this is now component testing where I have to manually build the nuxt component support. And part of that is mocking, for example, the $fetch function. Otherwise, it is just undefined, so intercept wouldn't work.

lmiller1990 commented 1 year ago

Right, I see. According to unplugin-auto-import this is in Nuxt out of the box. If you add that plugin to your vite.config and build the component, is $fetch available? I think this issue is attempting to work around an issue in the build step, where $fetch isn't injected. I don't think we should work around it like this, but we should instead inject the $fetch correctly.

I looked into this. I was able to reproduce what Nuxt does using unimport and read the Nuxt docs. Here's my example (Vue + Cypress, inject $fetch: https://github.com/lmiller1990/cypress-unimport-example/blob/main/vite.config.ts#L7-L18)

Nuxt's imports are here: https://github.com/nuxt/nuxt/blob/2edd7a34419e042f6a694929d32178ecb379be51/packages/nuxt/src/imports/presets.ts#L15 but it doesn't have $fetch, that seems to be a special case. I think they have logic somewhere for vueuse, but I don't know where. You could try https://github.com/nuxt/nuxt/issues/14534#issuecomment-1419025107, which might give you the entire Nuxt config in one shot.

What do you think?