How to test a global event bus in VueJS
Asked Answered
N

1

6

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?

Neckpiece answered 21/2, 2020 at 12:31 Comment(0)
R
3
  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 tests\unit\HelloWorld.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');
  });
});
Rockefeller answered 25/2, 2020 at 10:26 Comment(5)
Great answer! Many thanks! How would you refactor the first two tests to use localVue?Neckpiece
Is this approach mentioned somewhere in the official documentation? If not, could you please name the source from where you have it?Neckpiece
I would not refactor those to use localVue, as approach with mocks is fine for me for the first two tests. I found this solution here: forum.vuejs.org/t/how-to-mock-event-bus-in-vue-test-utils/22815Rockefeller
Yes, the same person has provided an answer at SO, which I have referred to in my question. However there was no further explanation. Especially the part where you mock the method before mounting the component was a mistery for me. I tried a similar approach, but with spying on the method after creating the wrapper i.e. mounting the component. I asked about the mocks, because this is a very simple example, in production I have a factory function for creating the wrapper.Neckpiece
"spying on the method after creating the wrapper i.e. mounting the component" will not work, because the component will be created with its own method. When you do jest.spyOn(wrapper.vm, 'changeName'); probably you are not reassigning changeName method properly, so component has still wrong reference.Rockefeller

© 2022 - 2024 — McMap. All rights reserved.