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

Spying on methods when using the composition api #775

Closed
maurer2 opened this issue Jul 18, 2021 · 15 comments
Closed

Spying on methods when using the composition api #775

maurer2 opened this issue Jul 18, 2021 · 15 comments

Comments

@maurer2
Copy link
Contributor

maurer2 commented Jul 18, 2021

Hello, it seems that when using the options api, it is recommended to create spies for methods before mounting to avoid issues with event handlers that don't have braces (example 1, example 2, example 3).
How would one do the same with the composition api? The following doesn't work, as methods isn't available/exposed when using the composition api?

const spy = jest.spyOn(ComponentName.methods, 'methodName');
// mount stuff
expect(spy).toHaveBeenCalled()

Thanks

Cheers

@cexbrayat
Copy link
Member

I think you should be able to do something like:

const wrapper = mount(ComponentName);
const spy = (wrapper.vm.methodName = jest.fn());
await wrapper.get('button').trigger('click');
expect(spy).toHaveBeenCalled();

Feel free to reopen this issue with a concrete example if that doesn't work, but I think I've used this pattern several times with success.

@maurer2
Copy link
Contributor Author

maurer2 commented Jul 18, 2021

Hello, thanks for looking into this. I created an example project with tests to demonstrate the issue (https://github.com/maurer2/vue3-testing-question). I have two two buttons that have click event handlers. One with parentheses and one without.

 <ButtonComponent
      id="button-2a"
      @click="handleClick1"
/>
    
<ButtonComponent
      id="button-2b"
      @click="handleClick2()"
/>

The second click handler can be tested like so:

  it('button2 triggers counter update function when clicked', async () => {
    const spy = jest.spyOn(cmp.vm, 'handleClick2');

    await cmp.find('#button-2b').trigger('click');
    expect(spy).toHaveBeenCalled();
  });

This doesn't work for the first one as the following test fails:

it('button1 triggers counter update function when clicked - spied on the fly', async () => {
    const spy = jest.spyOn(cmp.vm, 'handleClick1');

    await cmp.find('#button-2a').trigger('click');
    expect(spy).toHaveBeenCalled();
  });

This seems to only work when performing the spying before mounting the component like so:

let cmp: any
let spyHandleClick1: any;

 beforeEach(() => {
    spyHandleClick1 = jest.spyOn((ComponentOptionsAPI.methods as any), 'handleClick1');
    cmp = shallowMount(ComponentOptionsAPI, {});
  });

 it('button1 triggers counter update function when clicked - spied before mount', async () => {
    await cmp.find('#button-2a').trigger('click');

    expect(spyHandleClick1).toHaveBeenCalled();
  });

Unfortunately ComponentName.method doesn't seem to be available for components that are written using the composition api, so this workaround doesn't work there. What would be the equivalent to get this working other than always using parentheses for event handlers?

Cheers

@cexbrayat
Copy link
Member

Thanks for the use-case 👍

I sadly think we can't do much, as the generated code is fundamentally different between @click="handleClick1" and @click="handleClick2()".
See here

Is it really a problem to have to use parens if you want to spy on the handler?

I'll reopen in the meantime, maybe @lmiller1990 or @afontcu will have an idea on how to handle this.

@cexbrayat cexbrayat reopened this Jul 19, 2021
@maurer2
Copy link
Contributor Author

maurer2 commented Jul 19, 2021

Hello, thanks for reopening the issue. I guess it would make sense to always use parentheses when using the composition api and set v-on-style from eslint-plugin-vue (https://eslint.vuejs.org/rules/v-on-style.html) to always when using the composition api to avoid flaky tests.

I got a somewhat related question regarding the lifecycle methods. If I wanted to check if a function is called on mount, I would do it like before with the options api:

let spyRunOnMount: any;

beforeEach(() => {
    spyRunOnMount = jest.spyOn((ComponentOptionsAPI.methods as any), 'runOnMount');
    mount stuff
  });

it('run runOnMount runs on mount', async () => {
    expect(spyRunOnMount).toHaveBeenCalled();
});

With the composition api I would need to return runOnMount function from setup to expose it in the test, but when I register the spy the lifecycle hook has already run and the test fails:

let spyRunOnMount: any;

beforeEach(() => {
    mount stuff
    spyRunOnMount = jest.spyOn(cmp.vm, 'runOnMount');
  });

it('runOnMount runs on mount', async () => {
    expect(spyRunOnMount).toHaveBeenCalled();
  });

Is there some way to test this or is just not possible with components that use the composition api?

Cheers

@cexbrayat
Copy link
Member

In that case what I usually really want to check is not that the method itself was called, but rather what it does or that an external dependency was properly called. For example, if I fetch some data on mount, I'll check that the composable that does the HTTP call was properly called:

const mockRaceService = {
  list: jest.fn()
};
jest.mock('@/composables/RaceService', () => ({
  useRaceService: () => mockRaceService
}));

describe('Races.vue', () => {
  test('should display every race', async () => {
    const wrapper = mount(Races);
    await flushPromises();
    expect(mockRaceService.list).toHaveBeenCalled();

I hope this helps. I don't think we'll be able to allow to spy on the method as you would expect, because the model is fundamentally different. Or maybe with some hacky method, but I'm not really sure that's worth the complexity... We've been using the composition API for more than a year, and we manage to test everything in our projects.

@maurer2
Copy link
Contributor Author

maurer2 commented Jul 21, 2021

Okay, makes sense. Thanks for the explanation.

Cheers

@cexbrayat
Copy link
Member

You're welcome! If you don't mind, I'll close the issue for now, and we can revisit if some other use cases come up.

@tongvantruong
Copy link

It doesn't work on my side.

MyComponent.vue

<template>
  <div @click="methodA"> </div>
</template>

<script setup lang="ts">
const methodA = () => {
  console.log("methodA");
  methodB();
};

const methodB = () => {
  console.log("methodB");
};
</script>

MyComponent.spec.ts

import { mount } from "@vue/test-utils";
import MyComponent from "./MyComponent.vue";

it("a", () => {
  const wrapper = mount(MyComponent);
  const spy = jest.spyOn(wrapper.vm, "methodB");
  wrapper.vm.methodA();
  expect(spy).toHaveBeenCalled();
});

=> Error

 expect(jest.fn()).toHaveBeenCalled()

    Expected number of calls: >= 1
    Received number of calls:    0

       7 |   const spy = jest.spyOn(wrapper.vm, "methodB");
       8 |   wrapper.vm.methodA();
    >  9 |   expect(spy).toHaveBeenCalled();
         |               ^
      10 | });

@maurer2
Copy link
Contributor Author

maurer2 commented Oct 26, 2022

Hello, have you tried adding the parentheses to the event handlers? Vue permits both syntaxes, e.g.

<template>
  <div @click="methodA()"> </div>
</template>

If that doesn't fix the issue, you could turn it into a hook/composable.

@sureshvv
Copy link

Why is there no way to spy on a Composition API method before mount? I am having to go on a major refactoring spree to bypass this limitation. Please don't tell me it is for my own good :)

@lmiller1990
Copy link
Member

This is a limitation for <script setu>, it is closed by default unless you do defineExpose.

I don't understand why you need a major refactoring spree - can you elaborate? If you are only refactoring and you need to make make changes to your test code, that's probably a good indication you are testing implementation details (like spying on a specific method).

@sureshvv
Copy link

sureshvv commented Aug 28, 2023

I have a component method A that calls another method B only when certain conditions are met.

I want to test that by spyOn B.

Does not work.

So I made it a Composable.

My app worls fine. My tests do not.

When I export both A & B into my test. Set up the condition & call A. spyOn does not report that B is called.

See https://gist.githubusercontent.com/sureshvv/3c9825040be95692ae50181f537c9b39/raw/c89abc6009896acf69db0664824ca75ef94107cf/gistfile1.txt

@lmiller1990
Copy link
Member

What is the effect of calling sub1? I'd recommend asserting against that. If you do defineExpose are you able to spy on it?

@sureshvv
Copy link

sureshvv commented Aug 29, 2023

Let me try defineExpose. Have not used it before. The module is not a vue module, it is just a js file. I guess I will have to change that.

sub1() actually calls the database and pulls record out of it async. I have that mocked already.

@cexbrayat
Copy link
Member

defineExpose should not change anything, as Vue Test Utils bypasses it to make tests simpler. If it does, please open an issue.

If you already mock the database, then the easiest way to test if everything work is to assert that your mocked database is properly called instead of sub1.

See my comment above #775 (comment)

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

5 participants