In this article it is explained how to use a global event bus in VueJS. It describes an alternative method to the common approach using an event bus defined in a separate file:
import Vue from 'vue';
const EventBus = new Vue();
export default EventBus;
This has to be imported in every SFC where it is needed. The alternative approach attaches the global event bus to the main Vue instance:
// main.js
import Vue from 'vue';
Vue.prototype.$eventBus = new Vue(); // I call it here $eventBus instead of $eventHub
new Vue({
el: '#app',
template: '<App/>',
});
// or alternatively
import Vue from 'vue';
import App from './App.vue';
Vue.prototype.$eventBus = new Vue();
new Vue({
render: (h): h(App),
}).$mount('#app');
Now I have the problem that I don't know how to use the global event bus created in this way in the unit tests.
There is already a question about testing the global event bus using the first mentioned approach, but no answer is accepted.
I tried as suggested in one of the answers to use createLocalVue
, but that didn't help:
it('should listen to the emitted event', () => {
const wrapper = shallowMount(TestingComponent, { localVue });
sinon.spy(wrapper.vm, 'handleEvent');
wrapper.vm.$eventBus.$emit('emit-event');
expect(wrapper.vm.handleEvent.callCount).to.equal(1);
});
This says expected 0, actual 1. I tried with async
function and $nextTick()
but without success.
For the previous example I'm using mocha
, chai
and sinon
. This is just for illustration. Answers using jest
or any other testing framework / assertion library are highly appreciated.
EDIT on 25th February 2020
Reading the book "Testing Vue.js Applications" from the Edd Yerburgh, author of @vue/test-utils
, I came up with some ideas, but I'm still struggling to understand how to accomplish the testing of the global event bus added as an instance property. In the book instance properties are mocked in the unit tests.
I created a git repository with example code following the article from medium.com. For this example I used jest
for unit testing.
This is the code:
src/main.js
import Vue from 'vue';
import App from './App.vue';
// create global event bus as instance property
Vue.prototype.$eventBus = new Vue();
Vue.config.productionTip = false;
new Vue({
render: (h) => h(App),
}).$mount('#app');
src/App.vue
<template>
<div id="app">
<hello-world></hello-world>
<change-name></change-name>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue';
import ChangeName from './components/ChangeName.vue';
export default {
name: 'App',
components: {
HelloWorld,
ChangeName,
},
};
</script>
src/components/HelloWorld.vue
<template>
<div>
<h1>Hello World, I'm {{ name }}</h1>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
name: 'Foo',
};
},
created() {
this.$eventBus.$on('change-name', this.changeName);
},
beforeDestroy() {
this.$eventBus.$off('change-name');
},
methods: {
changeName(name) {
this.name = name;
},
},
};
</script>
src/components/ChangeName.vue
Change name
<script>
export default {
name: 'ChangeName',
data() {
return {
newName: '',
};
},
methods: {
changeName() {
this.$eventBus.$emit('change-name', this.newName);
},
},
};
</script>
It's a very simple application with two components. The component ChangeName.vue
has an input element and the user can trigger a method by clicking a button. The method emits an event change-name
using the global event bus. The component HelloWorld.vue
listens to the event change-name
and updates the model property name
.
Here is how I have tried to test it:
tests\unit\HelloWorld.spec.js
import { shallowMount } from '@vue/test-utils';
import HelloWorld from '@/components/HelloWorld.vue';
describe('HelloWorld.vue', () => {
const mocks = {
$eventBus: {
$on: jest.fn(),
$off: jest.fn(),
$emit: jest.fn(),
},
};
it('listens to event change-name', () => {
// this test passes
const wrapper = shallowMount(HelloWorld, {
mocks,
});
expect(wrapper.vm.$eventBus.$on).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$eventBus.$on).toHaveBeenCalledWith('change-name', wrapper.vm.changeName);
});
it('removes event listener for change-name', () => {
// this test does not pass
const wrapper = shallowMount(HelloWorld, {
mocks,
});
expect(wrapper.vm.$eventBus.$off).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$eventBus.$off).toHaveBeenCalledWith('change-name');
});
it('calls method changeName on event change-name', () => {
// this test does not pass
const wrapper = shallowMount(HelloWorld, {
mocks,
});
jest.spyOn(wrapper.vm, 'changeName');
wrapper.vm.$eventBus.$emit('change-name', 'name');
expect(wrapper.vm.changeName).toHaveBeenCalled();
expect(wrapper.vm.changeName).toHaveBeenCalledWith('name');
});
});
tests\unit\ChangeName.spec.js
import { shallowMount } from '@vue/test-utils';
import ChangeName from '@/components/ChangeName.vue';
describe('ChangeName.vue', () => {
const mocks = {
$eventBus: {
$on: jest.fn(),
$off: jest.fn(),
$emit: jest.fn(),
},
};
it('emits an event change-name', () => {
// this test passes
const wrapper = shallowMount(ChangeName, {
mocks,
});
const input = wrapper.find('input');
input.setValue('name');
const button = wrapper.find('button');
button.trigger('click');
expect(wrapper.vm.$eventBus.$emit).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$eventBus.$emit).toHaveBeenCalledWith('change-name', 'name');
});
});
TL;DR
It is a very long question, but most of it are code examples. The question is how to unit test a global event bus created as Vue instance property?
In particular I have a problem understanding the third test in tests/unit/HelloWorld.spec.js
. How do I check that the method is called when an event is emitted? Should we test this behaviour in unit tests at all?
localVue
? – Neckpiece