Monday, May 20, 2024
188
rated 0 times [  194] [ 6]  / answers: 1 / hits: 6430  / 4 Years ago, fri, february 21, 2020, 12:00:00

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:



testsunitHelloWorld.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');
});
});


testsunitChangeName.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?


More From » unit-testing

 Answers
5

  1. In test where you are checking if vm.$eventBus.$off listener was fired properly, you have to force component to destroy.

  2. In change name method test I added a few improvements:


    • I passed localVue with plugin that initialized eventHub

    • I removed eventHub mocks as they are no longer valid here

    • I mocked changeName method in component setup, not after a component is created




Here is my suggestion for testsunitHelloWorld.spec.js:



import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vue from 'vue';
import HelloWorld from '@/components/HelloWorld.vue';

const GlobalPlugins = {
install(v) {
v.prototype.$eventBus = new Vue();
},
};

const localVue = createLocalVue();
localVue.use(GlobalPlugins);

describe('HelloWorld.vue', () => {
const mocks = {
$eventBus: {
$on: jest.fn(),
$off: jest.fn(),
$emit: jest.fn(),
},
};

it('listens to event change-name', () => {
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', () => {
const wrapper = shallowMount(HelloWorld, {
mocks,
});

wrapper.destroy();
expect(wrapper.vm.$eventBus.$off).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$eventBus.$off).toHaveBeenCalledWith('change-name');
});

it('calls method changeName on event change-name', () => {
const changeNameSpy = jest.fn();
const wrapper = shallowMount(HelloWorld, {
localVue,
methods: {
changeName: changeNameSpy,
}
});

wrapper.vm.$eventBus.$emit('change-name', 'name');

expect(changeNameSpy).toHaveBeenCalled();
expect(changeNameSpy).toHaveBeenCalledWith('name');
});
});

[#4676] Wednesday, February 19, 2020, 4 Years  [reply] [flag answer]
Only authorized users can answer the question. Please sign in first, or register a free account.
lailab

Total Points: 706
Total Questions: 102
Total Answers: 95

Location: Falkland Islands
Member since Mon, Jul 13, 2020
4 Years ago
;