12

I am trying to learn how to test events emitted through a global Event Bus. Here's the code with some comments in the places I don't know what to do.

// EvtBus.js
import Vue from 'vue';
export const EvtBus = new Vue();
<!-- CouponCode.vue -->
<template>
    <div>
        <input
            class="coupon-code"
            type="text"
            v-model="code"
            @input="validate">
        <p v-if="valid">
            Coupon Redeemed: {{ message }}
        </p>
    </div>
</template>

<script>

import { EvtBus } from '../EvtBus.js';

export default {
    data () {
        return {
            code: '',
            valid: false,

            coupons: [
                {
                    code: '50OFF',
                    discount: 50,
                    message: '50% Off!'
                },
                {
                    code: 'FREE',
                    discount: 100,
                    message: 'Entirely Free!'
                }
            ]
        };
    },

    created () {
        EvtBus.$on('coupon-applied', () => {
            //console.info('had a coupon applied event on component');
        });
    },

    methods: {
        validate () {
            // Extract the coupon codes into an array and check if that array
            // includes the typed in coupon code.
            this.valid = this.coupons.map(coupon => coupon.code).includes(this.code);
            if (this.valid) {
                this.$emit('applied');
                // I NEVER see this on the coupon-code.spec.js
                EvtBus.$emit('coupon-applied');
            }
        }
    },

    computed: {
        message () {
            return this.coupons.find(coupon => coupon.code === this.code).message;
        }
    }
}
</script>
// tests/coupon-code.spec.js
import expect from 'expect';
import { mount } from '@vue/test-utils';
import CouponCode from '../src/components/CouponCode.vue';
import { EvtBus } from '../src/EvtBus.js';

describe('Reminders', () => {
    let wrp;

    beforeEach(() => {
        wrp = mount(CouponCode);
    });

    it('broadcasts the percentage discount when a valid coupon code is applied', () => {
        let code = wrp.find('input.coupon-code');
        code.element.value = '50OFF';
        code.trigger('input');

        console.log(wrp.emitted('applied'));

        //
        // I NEVER see this on the outpout.
        // How can I test it through a global event bus rather than
        // an event emitted from the component instance?
        //
        EvtBus.$on('coupon-applied', () => {
            console.log('coupon was applied through event bus');
        });

        // Passes, but not using EvtBus instance.
        expect(wrp.emitted('applied')).toBeTruthy;

    });
});

So, my doubt is how to test that the global event bus is emitting and listening to events inside components that use that event bus.

So, is it possible to test the global Event Bus using Vue Test Utils or I should use another approach?

Fernando Basso
  • 688
  • 1
  • 11
  • 25
  • Did you actually ever get anywhere with this? I'm in a similar situation and it's rendering testing almost useless for me. – Keith Jackson Aug 16 '19 at 13:18

5 Answers5

9

If component is using global EventBus, eg that's imported outside of given component and assigned to window.EventBus, then it's possible to use global Vue instance to redirect $on or $emit events to wrapper's vm instance. That way you can proceed writing tests as if component is emitting via this.$emit instead of EventBus.$emit:

it('clicking "Settings" button emits "openSettings"', () => {
    global.EventBus = new Vue();
    global.EventBus.$on('openSettings', (data) => {
        wrapper.vm.$emit('openSettings', data);
    });

    // component emits `EventBus.$emit('openSettings')`

    expect(wrapper.emitted('openSettings')).toBeTruthy(); // pass
});
Be Kind
  • 4,712
  • 1
  • 38
  • 45
2

Well,

EvtBus.$on('coupon-applied', () => {
    console.log('coupon was applied through event bus');
});

This code in your spec file won't be called because the mounted wrp component is not using the same EvtBus you are importing in your spec file above.

What you require to test this is an npm package named inject-loader so that you can provide your own implementation(stub) of the EvtBus dependency of your coupon code component.

Somewhat like this

const couponCodeInjector = require('!!vue-loader?inject!src/views/CouponCode');

const stubbedModules = {
   '../EvtBus.js': {
        $on : sandbox.spy((evtName, cb) => cb()); 
    }
};

const couponCode = couponCodeInjector(stubbedModules);

and then in your unit test you can assert whether the stubbedModules['../EvtBus.js'].$on has been called or not when code.trigger('input');

PS: I haven't used vue-test-utils. So I don't know exactly how to the stubbing with this npm package.

But the main thing you need to do is to find a way to stub your EvtBus dependency in the CouponCode component in such a way that you can apply a spy on it and check whether that spy has been called or not.

fullmetal
  • 414
  • 3
  • 11
  • I wish I could do without yet another dependency. Perhaps there is something in `vue-test-utils` to help me with this. I haven't read the entire docs yet. I do not understand your `$on : sandbox.spy(...)` thing and I will do some research on it today. – Fernando Basso Jan 29 '18 at 10:28
  • I tried but could not make your approach work yet. I'll let you know if I manage. Thanks for the help. – Fernando Basso Jan 29 '18 at 17:53
2

Unit tests should focus on testing a single component in isolation. In this case, you want to test if the event is emitted, since that is the job of CouponCode.vue. Remember, unit tests should focus on testing the smallest units of code, and only test one thing at a time. In this case, we care that the event is emitted -- EventBus.test.js is where we test what happens when the event is emitted.

Noe that toBeTruthy is a function - you need (). expect(wrp.emitted('applied')).toBeTruthy is actually not passing, since you need () - at the moment, it is actually doing nothing -- no assertion is made.

What your assertion should look like is:

expect(wrp.emitted('applied')).toBeTruthy()

You can go one step further, and ensure it was only emitted once by doing something like expect(wrp.emitted().applied.length).toBe(1).

You then test InputBus in isolation, too. If you can post the code for that component, we can work through how to test it.

I worked on a big Vue app recently and contributed a lot to the main repo and documentation, so I'm happy to help out wherever I can.

Let me know if that helps or you need more guidance. If possible, post EventBus.vue as well.

lmiller1990
  • 925
  • 2
  • 12
  • 22
  • Thanks for catching the missing `()` is `.toBeTruthy`. Note that altough I use `wrp.emitted`, it was just for illustration purposes, because my goal is to learn how to test a global event bus. `EvtBus.js` is included in the opening question. – Fernando Basso Jan 29 '18 at 10:26
  • The EvtBus can be tested, but it should be tested in isolation from this CouponCode.vue. I think you should not declare the EventBus events in CouponCode, but in EventBus.vue. – lmiller1990 Jan 29 '18 at 12:50
  • For now, your `CouponCode.vue` test is fine if you remove the `EventBus` -- you should only test that the event is correctly emitted here, this component should not even need to know about the existence of the CouponCode. In it's current for, you do not need to unit test `EventBus.vue.` You haven't written any custom logic to test there.a An integration test would be the place to test that `CouponCode` emits an event, `EventBus` responds -- this is not what `vue-test-utils` is made for, but something like Nightwatch or Selenium would be best for that. – lmiller1990 Jan 29 '18 at 12:58
  • Hey. I made a Gist to show you how. https://gist.github.com/lmiller1990/2c264448856853d139c10ca3c029df23 Basically, you want to make sure EventBus can be tested without other components. What I did was made a function that abstracts the `$on` into `EventBus.js` itself, and tested `EventBus` by itself. Unit tests should not need other components, or it's not a unit test. Let me know if the gist helps :) – lmiller1990 Jan 29 '18 at 13:14
  • Thanks. I kind of get your idea now. Not trying to be pedantic, but, still, why creating an `addEvent` method? Can't we test something like used here? https://vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication – Fernando Basso Jan 29 '18 at 17:52
  • Presumably you will be using the EventBus and adding many events in other components, so it makes sense to make a helper function to do so. It is also easier to test the EventBus, that way. Just to check, you want to test "when CouponCode emits X event, EventBus reacts in Y way"? – lmiller1990 Jan 30 '18 at 03:19
  • 1
    ComponentA emits an event through `EvtBus.$emit('foo', 10)`, and any other components react to the event through `EvtBus.$on('foo', ...)`. To me, it seems I wouldn't care about testing EvtBus because it just works in that plain simple way. We know Vue instances emit and listen to events. What I care is know that I emitted an event from ComponentA using EvtBus, and then in other components, I care that I can listen to such incoming event with EvtBus. Anyway, it is just my current understanding of th situation, which my very well be completely wrong. – Fernando Basso Jan 30 '18 at 09:43
  • This seems to be testing what I originally intended: https://gist.github.com/FernandoBasso/e3a0cfc5b2ba6d754833d1e07d33b16c – Fernando Basso Jan 30 '18 at 13:00
  • This still feels like a bit of a smell to me - I still do not believe you should be doing this in a unit test, it feels like you are testing the framework (whether Vue.$emit and Vue.$on work correctly (of course they do). However until you have some e2e tests, maybe this is okay - unless your test suits starts to run really slow, I don't think it hurts to have them. Seems like you have a solution that helps you test, and you are testing the components, which is the most important thing. – lmiller1990 Jan 31 '18 at 02:01
  • I agree it feels smelly and it does feel like I am testing the framework. I was more ore less convinced to test that an event was emitted by watching https://laracasts.com/series/testing-vue/episodes/5 Perhaps I just misunderstood the purpose. – Fernando Basso Jan 31 '18 at 09:05
1

I got the same issue with vue-test-utils and Jest. For me, createLocalVue() of vue-test-utils library fixed the issue. This function creates a local copy of Vue to use when mounting the component. Installing plugins on this copy of Vue prevents polluting the original Vue copy. (https://vue-test-utils.vuejs.org/api/options.html#localvue)

Adding this to your test file will fix the issue:

const EventBus = new Vue();

const GlobalPlugins = {
  install(v) {
    // Event bus
    v.prototype.$bus = EventBus;
  },
};

// create a local instance of the global bus
const localVue = createLocalVue();
localVue.use(GlobalPlugins);
Harinder Kaur
  • 91
  • 1
  • 8
  • 1
    Could you expand on how to use this? Your answer doesn't relate to the code in the question. I would like to use something like `wrp.vm.EvtBus.emitted('coupon-applied')`. – Adam Jagosz Aug 21 '19 at 11:35
1
jest.mock('@/main', () => ({
  $emit: jest.fn(),
}));

Include this in code in your spec file at the very begining.

Note: '@/main' is the file from which you are importing Event Bus.

m02ph3u5
  • 3,022
  • 7
  • 38
  • 51