How to Test a Global Event Bus With Vue Test Utils?
Asked Answered
S

5

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?

Skyscraper answered 28/1, 2018 at 12:22 Comment(1)
Did you actually ever get anywhere with this? I'm in a similar situation and it's rendering testing almost useless for me.Dulciedulcify
T
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
});
Townswoman answered 7/8, 2018 at 8:35 Comment(6)
For an ES6 style component attaching it to a global / window object is not good practice. Is this the only way to make it testable?Dulciedulcify
I create a EventBus property with value new Vue() inside of moks property on options object provided to shallowMount.Hermia
@DiegoAlbertoZapataHäntsch, could you post an example?Nomism
@Nomism you can see an extract of my configuration test here codesandbox.io/s/proud-butterfly-943tn?file=/src/index.jsHermia
Thanks, @DiegoAlbertoZapataHäntsch, so you use something like expect($EventBus.$emit).toHaveBeenCalledWith('event-name') instead of expect($EventBus.emitted('event-name')).toHaveLength(1). Could you show examples for assets?Nomism
@Nomism yes. I updated the example with an assert.Hermia
L
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.

Lustre answered 28/1, 2018 at 13:45 Comment(2)
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.Skyscraper
I tried but could not make your approach work yet. I'll let you know if I manage. Thanks for the help.Skyscraper
D
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.

Drury answered 28/1, 2018 at 15:1 Comment(10)
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.Skyscraper
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.Drury
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.Drury
Hey. I made a Gist to show you how. 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 :)Drury
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? vuejs.org/v2/guide/…Skyscraper
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"?Drury
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.Skyscraper
This seems to be testing what I originally intended: gist.github.com/FernandoBasso/e3a0cfc5b2ba6d754833d1e07d33b16cSkyscraper
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.Drury
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 laracasts.com/series/testing-vue/episodes/5 Perhaps I just misunderstood the purpose.Skyscraper
B
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);
Belinda answered 5/10, 2018 at 19:31 Comment(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').Kasper
L
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.

Ligetti answered 4/2, 2020 at 15:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.