Vue-test-utils: Mock $refs

Created on 15 Dec 2017  Â·  30Comments  Â·  Source: vuejs/vue-test-utils

It would be useful to be able to mock $refs.

Currently, as far as I understand there is no way to test a method that calls a childs methods via $refs.

methodToTest ( ) {
  this.a = 1
  this.$refs.childComponent.childsMethod()
}
---
undefined is not an object (evaluating 'this.$refs.childComponent.childsMethod')
feature request

Most helpful comment

Hi @ddykhoff, thanks for the fiddle.

The reason you can't call the method on the ref is because shallow stubs all the child components. The $refs object is still populated, but the toast component doesn't have any methods (because it's been stubbed).

You can use mount instead of shallow if you want to interact with a child components methods. Alternatively, you could use shallow and pass a custom stub:

const VueToastStub = {
  render: () => {},
  methods: {
    setOptions: () => {}
  }
}

const wrapper = shallow(Approvals, {
  stubs: {
    'vue-toast': VueToastStub
  }
})

All 30 comments

@charliekassel could you provide an example where you would want to use this functionality?

Perhaps it's actually stubbing rather than mocking.
For the example above where we have that method to test, I guess I would want the shallow constructor to stub that child method so it's not called, preventing the test throwing an error.
I want to test the logic contained in that methodToTest method without the side effects of calling $refs.childComponent.childsMethod()

I have a component Approvals.vue utilizing the vue-toast library. I don't need to call the library methods when testing my component.

<template>
    <div>
        {{ someContent }}
    </div>
    <vue-toast ref='toast'></vue-toast>
</template>

<script>
import VueToast from 'vue-toast';

export default {
    props: ['someContent'],
    components: {
        'vue-toast': VueToast
    },
    methods: {
      showToast() {
          const toast = this.$refs.toast;
          toast.showToast(this.someContent);
      },
  },
    mounted () {
        this.$refs.toast.setOptions({
            maxToasts: 3,
            position: 'bottom right'
        });
    }
}
</script>

And my test

describe('Approvals page', () => {
    const wrapper = shallow(Approvals);

    it('any test', () => {
        expect(wrapper).toBeDefined();
    });
});

This throws the following error and fails the test:

TypeError: Cannot read property 'setOptions' of undefined

Stubbing $refs would allow be to test this component, as is I cannot test anything in the Approvals component due to this issue.

Wouldn't that be solvable by stubbing the component that's being referenced?

messing with $refs means messing with the implementation of the component under test, which is not a good idea in my opinion.

@LinusBorg shallow is already stubbing the referenced component so my example still stands. The issue appears to be that the $refs are not populated in testing like they are in production.

@ddykhoff can you post a code example of $refs not being populated correctly in vue-test-utils?

@eddyerburgh My original comment in the issue has the example I am facing the issue with (https://github.com/vuejs/vue-test-utils/issues/271#issuecomment-353083583). Let me know if there is more info I can provide.

@eddyerburgh I was trying to create a JSFiddle to show the issue but I'm not sure its possible to transpile within JSFiddle, or at least I don't know how. Thus vue-test-utils cannot be loaded properly.

https://jsfiddle.net/7kgkfrbu/1/

Uncaught ReferenceError: require is not defined

Should I open another issue for this? Being able to provide JSFiddles is useful for bug reproduction.

@ddykhoff There's an iife build that runs in browsers—https://github.com/vuejs/vue-test-utils/blob/dev/dist/vue-test-utils.iife.js. Once we've released we could add it to CDNJS. At the moment, your best bet is to copy the code from the iife into JSFiddle. You also need to include vueTemplateCompiler (https://cdn.jsdelivr.net/npm/[email protected]/browser.js).

@eddyerburgh I got it working in JSFiddle. You can use rawgit.com as a "CDN" for the iife build. Here is the bug repro you requested (child components methods not available in $refs).

https://jsfiddle.net/7kgkfrbu/6/

As you can see from the console output in the mounted callback, $refs is not populated.

Hi @ddykhoff, thanks for the fiddle.

The reason you can't call the method on the ref is because shallow stubs all the child components. The $refs object is still populated, but the toast component doesn't have any methods (because it's been stubbed).

You can use mount instead of shallow if you want to interact with a child components methods. Alternatively, you could use shallow and pass a custom stub:

const VueToastStub = {
  render: () => {},
  methods: {
    setOptions: () => {}
  }
}

const wrapper = shallow(Approvals, {
  stubs: {
    'vue-toast': VueToastStub
  }
})

So if that use case isn't one after all, I posit again: we don't have to mock refs, we can stub the components.

And we shouldn't either, because we create situations that are not possible in a real app.

@eddyerburgh I can confirm that the above technique worked for me and allowed me to test the parent method without error.

@eddyerburgh Is there an issue with referencing the $refs in the mounted callback? Using your suggestion I still see the same error

https://jsfiddle.net/7kgkfrbu/7/

@ddykhoff That is an issue with using an HTML template instead of a template property. If you refactor your example to use a template property, it works as expected.

vue-test-utils doesn't support HTML templates at the moment, if you'd like to see it as a feautre, please make a new feature request issue.

I'm going to close this issue as there are solutions to mock refs by stubbing components without a method that could potentially cause bugs.

How can I mock $refs on the component I'm testing. If I have a computed function which calculates whether or not a div is "too tall" I need to do this.$refs.ref.clientHeight which is not accessible when testing.

Because of this I need a way to mock $refs.

Well, if clientHeight isn't a real thing in a unit test anyway, why bother to test it in the context of a real component instance?

If you want to test the computed property (which smells a lot like testing implementation details), just test that function on its own, providing a simple componentInstance mock with .call(mockObject):

// your computed property
function isTooTall() {
  return this.someData - this$refs.ref.clientHeight < 100
}
// your test
const mock = {
  someValue: 1000,
  $refs: {
    ref: {
      clientHeight: 910
    }
  }
}
const result YourComponent.computed.isTooTall.call(mock)

expect(result).toBe(true)

Generally, stuff involving dimensions should probably be tested in an end-to-end test where you can verify the correctness of the resulting behaviour (breakpoints, change of dimensions etc), not a unit test that tests implementation details. That computed prop could calculate correctly and a dozen other things only appearant in a real e2e test could make the behaviour relying on this property break.

@LinusBorg I'm primarily trying to just get the component to complete the render cycle without throwing an error. I guess I could just check if this.$refs has the property ref.

Im also having the same issue. I need to mock the clientWidth of an element that I'm using a $ref to retrieve. My functionality involves dynamically translating an object based on the width of the container. It's unfortunate that the $ref doesn't get updated when I modify the elements in the same way the event.target gets updated https://vue-test-utils.vuejs.org/api/wrapper/#trigger-eventtype-options

same question about test $refs from element-ui form component:

submitFilter() {
    this.$refs.filter.validate((valid) => {
        if (valid) {
              this.$emit('submitFilter', this.formData)
          }
    })
}

Here is my part code of test:

it('submit data', () => {
    const wrapper = shallowMount(OrdersFilter, {
      localVue
    })
    wrapper.vm.formData = {
      value: 'form'
    }
    wrapper.vm.submitFilter()
    console.log(wrapper.emitted())
  })

I want to test if wrapper.emmitted, but jest throw an error about

this.$refs.filter.validate is not a function

still not familar with vue-test-utils, anyone can help? thanks in advance.

@XiaYuYing I have the same problem with element-ui, any ideas?

@krskibin I had same problem with element-ui..
I encourage you to use proper framework like:
vuiefy

Example workarounds might be:

  1. Just use e2e test with cypress, or
  2. in code of your component
if (!this.$refs.filter.validate) {
  return false
}

Option 2 is how bad testing practices can destroy codebase...
So I would choose Option1.

Weird syntax, no? As this is creating an object, not a Vue component:

const VueToastStub = {
  render: () => {},
  methods: {
    setOptions: () => {}
  }
}

const wrapper = shallow(Approvals, {
  stubs: {
    'vue-toast': VueToastStub
  }
})

Might work in standard JS, but TS is very unhappy with that. I had to do this in my code:

    // @ts-ignore
    const StubbedCropper = Vue.extend({
      methods: {
        getCroppedCanvas: () => 'placeholder',
      },
      render: () => ({}),
    });

@xyyVee You can try this

import { shallowMount } from '@vue/test-utils';
import { Form } from 'element-ui';
import component from '.@/components/index';

describe('index.vue', () => {
    beforeEach(() => {
        wrapper = shallowMount(component, {
            stubs: {
                'el-form': Form,
            },
        });
    });
});

same question about test $refs from element-ui form component:

submitFilter() {
    this.$refs.filter.validate((valid) => {
        if (valid) {
              this.$emit('submitFilter', this.formData)
          }
    })
}

Here is my part code of test:

it('submit data', () => {
    const wrapper = shallowMount(OrdersFilter, {
      localVue
    })
    wrapper.vm.formData = {
      value: 'form'
    }
    wrapper.vm.submitFilter()
    console.log(wrapper.emitted())
  })

I want to test if wrapper.emmitted, but jest throw an error about

this.$refs.filter.validate is not a function

still not familar with vue-test-utils, anyone can help? thanks in advance.

For me helped the way of changing shallowMount to mount, and mocking the ref method like this:

const mockedMethod = jest.fn()
wrapper.vm.$refs.childComponent.someMethod = mockedMethod

// link where parent method calls this.$refs.childComponent.someMethod
link.trigger('click')

await flushPromises()

expect(mockedMethod).toHaveBeenCalled()

Hope this may be useful for someone.

@xyyVee You can try this

import { shallowMount } from '@vue/test-utils';
import { Form } from 'element-ui';
import component from '.@/components/index';

describe('index.vue', () => {
    beforeEach(() => {
        wrapper = shallowMount(component, {
            stubs: {
                'el-form': Form,
            },
        });
    });
});

same question about test $refs from element-ui form component:

submitFilter() {
    this.$refs.filter.validate((valid) => {
        if (valid) {
              this.$emit('submitFilter', this.formData)
          }
    })
}

Here is my part code of test:

it('submit data', () => {
    const wrapper = shallowMount(OrdersFilter, {
      localVue
    })
    wrapper.vm.formData = {
      value: 'form'
    }
    wrapper.vm.submitFilter()
    console.log(wrapper.emitted())
  })

I want to test if wrapper.emmitted, but jest throw an error about

this.$refs.filter.validate is not a function

still not familar with vue-test-utils, anyone can help? thanks in advance.

I have the same question about the element UI validate not a function error, do you fix this problem? waiting~~

This is awkward. In my case, the tests pass when ran through WebStorm, but don't pass running on terminal.

Basically because something I'm doing with $refs which in here are just simple HTML elements, not components. Same problem:

 TypeError: Cannot read property 'contains' of undefined

      217 |     },
      218 |     clickedOutsideMainMenu(e) {
    > 219 |       if (this.$refs.mainMenuOpenBtn.contains(e.target)) return
          | ^
      220 |       this.isMainMenuOpen = this.$refs.mainMenu.contains(e.target)
      221 |     },
      222 |   },

this.$refs.mainMenuOpenBtn is just a simple <button>.

Switching from shallowMount to mount makes no difference.

Edit
I think I figured out why the tests work on WebStorm. Maybe because I ran them while debugging with breakpoints. If that's the reason, this might be happening due something that is not ready yet while I run them at the terminal. Anyway, didn't solve the problem, actually it got more weird, since I'm using an async/await methods with $nextTick but it still does not work at the terminal.

For me helped the way of changing shallowMount to mount, and mocking the ref method like this:

const mockedMethod = jest.fn()
wrapper.vm.$refs.childComponent.someMethod = mockedMethod

// link where parent method calls this.$refs.childComponent.someMethod
link.trigger('click')

await flushPromises()

expect(mockedMethod).toHaveBeenCalled()

Hope this may be useful for someone.

It was, thanks! I was trying to put the $refs object in mocks within the mount options but never thought of doing it like this :D

For those simply wanting to access a child method from a mounted component (similarly to how a parent component would use this.$refs.componentRef.childMethod()) you can simply call wrapper.vm.childMethod(), assuming you set up your mount as such:

const wrapper = mount(YourComponent, {
   ...
};
Was this page helpful?
0 / 5 - 0 ratings

Related issues

robcresswell picture robcresswell  Â·  3Comments

dlumbrer picture dlumbrer  Â·  3Comments

alexanderstudte picture alexanderstudte  Â·  3Comments

vilarinholeo picture vilarinholeo  Â·  3Comments

38elements picture 38elements  Â·  3Comments