【问题标题】:How to test a global event bus in VueJS如何在 VueJS 中测试全局事件总线
【发布时间】:2020-02-21 12:31:33
【问题描述】:

article 解释了如何在 VueJS 中使用全局事件总线。它描述了使用在单独文件中定义的事件总线的通用方法的替代方法:

import Vue from 'vue';

const EventBus = new Vue();
export default EventBus;

这必须在每个需要它的 SFC 中导入。另一种方法是将全局事件总线附加到主 Vue 实例:

// 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');

现在我有一个问题,我不知道如何在单元测试中使用以这种方式创建的全局事件总线。

已经有一个question关于使用第一个提到的方法测试全局事件总线,但没有接受任何答案。

我按照answers 之一的建议尝试使用createLocalVue,但这没有帮助:

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);
});

这表示预期为 0,实际为 1。我尝试使用 async 函数和 $nextTick() 但没有成功。

对于前面的示例,我使用mochachaisinon。这只是为了说明。非常感谢使用 jest 或任何其他测试框架/断言库的答案。

2020 年 2 月 25 日编辑

阅读 @vue/test-utils 的作者 Edd Yerburgh 的书 "Testing Vue.js Applications",我想出了一些想法,但我仍然在努力理解如何完成对作为实例属性添加的全局事件总线的测试.在书中实例属性在单元测试中被模拟。

我在 medium.comarticle 之后创建了一个带有示例代码的 git repository。对于这个例子,我使用jest 进行单元测试。

这是代码:

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 更换名字

<script>
export default {
  name: 'ChangeName',
  data() {
    return {
      newName: '',
    };
  },
  methods: {
    changeName() {
      this.$eventBus.$emit('change-name', this.newName);
    },
  },
};
</script>

这是一个非常简单的应用程序,包含两个组件。组件ChangeName.vue 有一个输入元素,用户可以通过点击一个按钮来触发一个方法。该方法使用全局事件总线发出事件change-name。组件HelloWorld.vue监听事件change-name并更新模型属性name

这是我尝试测试的方法:

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

这是一个很长的问题,但大部分是代码示例。问题是如何对创建为 Vue 实例属性的全局事件总线进行单元测试?

特别是我在理解tests/unit/HelloWorld.spec.js 中的第三个测试时遇到了问题。如何检查发出事件时是否调用了该方法?我们应该在单元测试中测试这种行为吗?

【问题讨论】:

    标签: javascript unit-testing vue.js vue-test-utils vue-events


    【解决方案1】:
    1. 在您检查vm.$eventBus.$off 侦听器是否正确触发的测试中,您必须强制组件销毁。
    2. 在更改名称方法测试中,我添加了一些改进:
      • 我通过 localVue 使用初始化 eventHub 的插件
      • 我删除了 eventHub 模拟,因为它们在这里不再有效
      • 我在组件设置中模拟了changeName 方法,而不是在创建组件之后

    这是我对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');
      });
    });
    

    【讨论】:

    • 很好的答案!非常感谢!您将如何重构前两个测试以使用 localVue
    • 官方文档中是否提到过这种方法?如果没有,您能否说出您拥有它的来源?
    • 我不会重构那些以使用 localVue,因为对于前两个测试,使用 mock 的方法对我来说很好。我在这里找到了这个解决方案:forum.vuejs.org/t/how-to-mock-event-bus-in-vue-test-utils/22815
    • 是的,同一个人在 SO 提供了答案,我在我的问题中提到了这一点。然而没有进一步的解释。尤其是在安装组件之前模拟该方法的部分对我来说是一个谜。我尝试了一种类似的方法,但是在创建包装器(即安装组件)之后监视该方法。我询问了模拟,因为这是一个非常简单的示例,在生产中我有一个用于创建包装器的工厂函数。
    • “在创建包装后监视方法,即安装组件”将不起作用,因为组件将使用自己的方法创建。当您执行 jest.spyOn(wrapper.vm, 'changeName'); 时,可能您没有正确重新分配 changeName 方法,因此组件仍然有错误的引用。
    猜你喜欢
    • 2018-07-07
    • 2019-08-19
    • 1970-01-01
    • 2016-10-30
    • 2018-11-24
    • 2017-10-08
    • 2022-01-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多