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

Bug: Spying on setup methods doesn't work as expected #1718

Closed
twisteriokovel opened this issue Aug 11, 2022 · 15 comments
Closed

Bug: Spying on setup methods doesn't work as expected #1718

twisteriokovel opened this issue Aug 11, 2022 · 15 comments
Labels
bug Something isn't working

Comments

@twisteriokovel
Copy link

Describe the bug
I faced a strange issue during testing my application. Please see this example for better context.
Imagine you want to test the Counter component and incrementCounter method in it. Simple test example should look like this:

test('should react on click event', async () => {
    const wrapper = shallowMount(Counter);
    const incrementCounterFn = vi.spyOn(wrapper.vm, 'incrementCounter');

    await wrapper.find('button').trigger('click');

    expect(incrementCounterFn).toHaveBeenCalled();
});

But I get a strange error:

AssertionError: expected "incrementCounter" to be called at least once

At the same time, I have one more component Conter2. It is the same component, but it uses an "old" version of setup function and tests for this component work fine.
Can someone help me with this?

To Reproduce
Repo example

Expected behavior
Both tests should run without any errors

Related information:

  • @vue/test-utils version: 2.0.2
  • Vue version: 3.2.37
  • node version: 16.14.2
  • npm (or yarn) version: 7.17.0
@twisteriokovel twisteriokovel added the bug Something isn't working label Aug 11, 2022
@kostikovk
Copy link

kostikovk commented Aug 11, 2022

have the same problem

@walmartwarlord
Copy link

Related to my issue vuejs/vue-test-utils#1995

@wobsoriano
Copy link
Contributor

wobsoriano commented Aug 13, 2022

The only way to get around this issue is to put that function into a composable or another file

function useCounter() {
  const count = ref(0)

  function incrementCounter() {
    count.value++
  }

  return {
    count,
    incrementCount
  }
}
const mockIncrement = vi.fn()
vi.mock('@/composables/counter', () => ({
  useCounter () {
    return {
      incrementCounter: mockIncrement,
    }
  },
}))

test('should react on click event', async () => {
    const wrapper = shallowMount(Counter)

    await wrapper.find('button').trigger('click')

    expect(mockIncrement).toHaveBeenCalled()
})

Either that or use Options API 🤣

I know it's overkill if that function is only used in a single component.

And here's a related issue #775

@freakzlike
Copy link
Collaborator

freakzlike commented Aug 14, 2022

I think this is related to the fact, that the component internal instance is closed and (normally) not accessible from outside.

In your case I would:

  1. Test the real behavior (as a user), and not rely on implementation details

or if not possible/too complicated to test

  1. Use a composable and mock it (as @wobsoriano did)

IMO when it is too complicated to test, then it is no overkill to move it into another file. So the composable can be tested in isolation

@wobsoriano
Copy link
Contributor

@freakzlike In our current app, we went with option 1 since we have tons of components and we don't want to create a separate composable for each one

@freakzlike
Copy link
Collaborator

@freakzlike In our current app, we went with option 1 since we have tons of components and we don't want to create a separate composable for each one

Of course option 1 is the best solution. Mocking something is always a risk of having wrong integrations and therefore untested use cases.

I will close this issue for now. Feel free to reopen, when one if my solution does not fit your needs

@twisteriokovel
Copy link
Author

@freakzlike @wobsoriano thanks for your answers
It works with the separate composable as expected and you are right saying that it is better to do a kind of "user testing", I mean don't rely on function call, but on the actual result. But sometimes, you can face the situation that you cannot check the result and you need to check whether the function has been called. For example, I have a problem with forcing the focus state. If you don't have a separated variable, let's say isFocused in your setup, you cannot determine if the element was focused. I had some trouble accessing document.activeElement during the mounting, so checking if the function had been called was the only option.
Do you have any plans to add the support for "new" setup support?

@freakzlike
Copy link
Collaborator

I don't think this will be added as it is also not possible to spy on a function call within a js module. I would recommend you to use some real browser E2E testing (like Cypress) for such cases.

@felixzapata
Copy link

I have a couple of basic components that react to a click interaction. On one of them, we can spy the method making a cast (in order to avoid a typing error) of the object like it is described here, for example:

const wrapperCast = (wrapper.vm as unknown as { openTab: () => void; });
const spy = vi.spyOn(wrapperCast, 'openTab');

and it works (the test fails/passes depending on the changes we made).

On the other hand, the other test is very similar to the first one, and even reduce it to the minimum expression we can't spy the method:

<script setup lang="ts">

function onLogout() {
  console.log('foobar')
}

</script>

<template>
  <button type="button"
    data-testid="logout-button"
    @click="onLogout"
  >
    Logout
</button>
</template>

I know that the above test maybe it makes sense to test via an E2E tool because it is a direct reaction: the user clicks - the component reacts, but I think it should be available at least a way to spy these kinds of methods without the option to move it to an external file or using something like Cypress or writing my component using the Options API.

@freakzlike
Copy link
Collaborator

I'm not sure if this works but you can try this:

<script setup lang="ts">
function onLogout() {
  console.log('foobar')
}
const helper = {
  onLogout
}
</script>

<template>
  <button @click="helper.onLogout">Logout</button>
</template> 
const spy = vi.spyOn(wrapper.vm.helper, 'onLogout')

@felixzapata
Copy link

For this particular example it works, thanks. But if I apply this solution to the full component it fails (maybe because of another collateral effect or whatever that I need to check).

Anyway, although your solution works, it is a little weird if finally, I need to wrap all the methods in an object "like" if we are exposing them using defineExpose.

@cexbrayat
Copy link
Member

I think it also probably works if you add the parenthesis to the onLogout call: @click="onLogout()"

See #1769 (comment)

@felixzapata
Copy link

Yep, it also works thanks. I forgot to mention that although it is weird to wrap the methods, I would prefer to do this instead of other possible options commented on before.

@cexbrayat
Copy link
Member

Yeah I think it makes more sense: you would add the parenthesis if you had a parameter anyway @click=onLogout($event). In my teams we prefer to always have the parenthesis and be consistent.

The only way we get rid of this behavior would be to change the generated code by Vue itself, but I'm not sure that's worth it.

@felixzapata
Copy link

I've just realized that the difference between the test I had working and the ones who not, was the parenthesis (the working test already had them because of a parameter)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

7 participants