Vue 测试速成班

  • 原文地址:dev.to/blacksonic/…
  • 原文做者:Gábor Soós
  • 译者:马雪琴
  • 声明:本翻译仅作学习交流使用,转载请注明来源

在你快要完成一个项目时,忽然工程里的不少地方都出现了 bug,你修完一个又冒出新的一个,就像在玩打地鼠游戏同样……几轮下来,你会感到一团糟。此时有一个可让你的项目再次发光的解救方案,那就是为将要开发的和已经存在的特性编写测试。编写测试能够保证功能特性没有 bug。html

在本教程中,我将向你展现如何为 Vue 应用程序编写单元、集成和端到端测试。vue

有关更多测试示例,能够查看个人 Vue TodoApp 实现ios

1. 类型

咱们能够编写三种类型的测试:单元测试、集成测试和端到端测试。下面这个金字塔能够帮助咱们理解这些测试类型。git

在金字塔下端的测试写起来更容易,运行起来更快,也更容易维护。可是,为何咱们不能只写单元测试呢?由于金字塔上端的测试能够帮助咱们检查系统里的各个组件之间是否能很好地协同工做,使咱们对系统更有把握。github

单元测试只能被单独使用在单个代码单元(类、函数)里;集成测试能够检查多个单元是否能按预期协同工做(组件层次结构、组件 + 存储);端到端测试则是从外部世界观察应用程序:浏览器及其交互。vue-router

2. 测试运行器

对于新的 Vue 项目,添加测试的最简单方法是使用 Vue CLI。在生成项目(执行 vue create myapp)时,你必须手动选择单元测试和 E2E 测试。vuex

安装完成后,package.json 中将出现下面几个附加依赖项:vue-cli

  • @vue/cli-plugin-unit-mocha: 使用 Mocha 进行单元/集成测试的插件
  • @vue/test-utils: 单元/集成测试的工具库
  • chai: 断言库 Chai

从如今开始,单元/集成测试文件可使用 *.spec.js 后缀写在 tests/unit 目录中。测试的目录不是硬连线的,你能够用下面的命令行参数来修改它:json

vue-cli-service test:unit --recursive 'src/**/*.spec.js'
复制代码

recursive 参数告诉测试运行器依据后面的通配符模式来搜索测试文件。axios

3. 单元测试

到目前为止,一切顺利,可是咱们尚未编写任何测试。接下来咱们将编写第一个单元测试!

describe('toUpperCase', () => {
  it('should convert string to upper case', () => {
    // 准备
    const toUpperCase = info => info.toUpperCase();

    // 操做
    const result = toUpperCase('Click to modify');

    // 断言
    expect(result).to.eql('CLICK TO MODIFY');
  });
});
复制代码

上面的例子验证了 toUpperCase 函数是否将传入的字符串转换为了大写字母。

首先是准备工做,导入函数、实例化对象并设置其参数,让目标对象(这里是一个函数)进入一个可测试的状态。而后操做该功能/方法。最后咱们对函数返回的结果进行断言。

Mocha 提供了 describeit 两个方法。describe 函数表示围绕测试单元组织测试用例:测试单元能够是类、函数、组件等。Mocha 没有内置的断言库,因此咱们必须使用 Chai :它能够设置对结果的指望。Chai 有许多不一样的内置断言,但没有涵盖全部用例,缺失的断言能够经过 Chai 的插件系统导入。

大多数时候,你还将为组件层次结构以外的业务逻辑编写单元测试,例如,状态管理或后端 API 处理。

4. 组件展现

下一步是为组件编写集成测试。集成测试不仅是测试 Javascript 代码,还会测试 DOM 和相应组件逻辑之间的交互。

// src/components/Footer.vue
<template>
  <p class="info">{{ info }}</p>
  <button @click="modify">Modify</button>
</template>
<script>
  export default {
    data: () => ({ info: 'Click to modify' }),
    methods: {
      modify() {
        this.info = 'Modified by click';
      }
    }
  };
</script>
复制代码

咱们测试的第一个组件是一个渲染其状态并在单击按钮时修改状态的组件。

// test/unit/components/Footer.spec.js
import { expect } from 'chai';
import { shallowMount } from '@vue/test-utils';
import Footer from '@/components/Footer.vue';

describe('Footer', () => {
  it('should render component', () => {
    const wrapper = shallowMount(Footer);

    const text = wrapper.find('.info').text();
    const html = wrapper.find('.info').html();
    const classes = wrapper.find('.info').classes();
    const element = wrapper.find('.info').element;

    expect(text).to.eql('Click to modify');
    expect(html).to.eql('<p class="info">Click to modify</p>');
    expect(classes).to.eql(['info']);
    expect(element).to.be.an.instanceOf(HTMLParagraphElement);
  });
});
复制代码

要在测试中渲染组件,咱们必须使用 Vue 测试工具库中的 shallowMountmount。这两个方法都会渲染组件,可是 shallowMount 不会渲染子组件(子元素将是空元素)。当须要引入某个组件进行测试时,咱们能够以相对路径引用 ../../../src/components/Footer.vue 或使用别名 @,路径开头的 @ 符号表示对源文件夹 src 的引用。

咱们可使用 find 选择器在渲染的 DOM 中搜索并获取它的 HTML、文本、类名或原生 DOM 元素。若是搜索的是一个可能不存在的片断,咱们可使用 exists 方法判断它是否存在。上述各类断言只是为了示意各类状况,实际在测试用例中写其中一个断言就够了。

5. 组件交互

咱们已经测试了 DOM 的渲染,但尚未与组件进行任何交互。咱们能够经过 DOM 或组件实例与组件交互:

it('should modify the text after calling modify', () => {
  const wrapper = shallowMount(Footer);

  wrapper.vm.modify();

  expect(wrapper.vm.info).to.eql('Modified by click');
});
复制代码

上面的例子展现了如何使用组件实例来实现交互。咱们可使用 vm 属性访问组件实例,还能够经过组件实例访问到组件 method 中的方法和 data 对象(状态)里的属性。

另外一种方法是经过 DOM 与组件交互,咱们能够触发按钮上的单击事件并观察是否显示文本:

it('should modify the text after clicking the button', () => {
  const wrapper = shallowMount(Footer);

  wrapper.find('button').trigger('click');
  const text = wrapper.find('.info').text();

  expect(text).to.eql('Modified by click');
});
复制代码

触发 buttonclick 事件等同于在组件实例上调用 modify 方法。

6. 父子组件交互

上面咱们单独测试了组件,但实际应用程序由多个部分组成。父组件经过 props 与子组件通讯,子组件经过触发事件与父组件通讯。

咱们能够经过修改传入组件的 props 来更新组件的展现文案,并经过事件将改动通知给父组件。

export default {
  props: ['info'],
  methods: {
    modify() {
      this.$emit('modify', 'Modified by click');
    }
  }
};
复制代码

在接下来的测试中,咱们须要把 props 做为输入,并监听触发的事件。

it('should handle interactions', () => {
  const wrapper = shallowMount(Footer, {
    propsData: { info: 'Click to modify' }
  });

  wrapper.vm.modify();

  expect(wrapper.vm.info).to.eql('Click to modify');
  expect(wrapper.emitted().modify).to.eql([
    ['Modified by click']
  ]);
});
复制代码

shallowMountmount 方法的第二个参数是一个可选参数,咱们可使用 propsData 设置输入的 props。触发的事件能够经过调用 emitted 方法得到,获得的结果是一个对象,key 是事件的名称,value 是事件参数数组。

6. store 集成

在前面的例子中,状态都在组件内部。而在复杂的应用程序中,咱们须要在不一样的位置访问和改变相同的状态。Vuex 是 Vue 的状态管理库,它能够帮助你在一个地方组织状态管理,并确保其可预测地发生变化。

const store = {
  state: {
    info: 'Click to modify'
  },
  actions: {
    onModify: ({ commit }, info) => commit('modify', { info })
  },
  mutations: {
    modify: (state, { info }) => state.info = info
  }
};
const vuexStore = new Vuex.Store(store);
复制代码

上面的 store 有一个单一的状态属性,它与咱们在上面的组件中设置的同样。咱们可使用 onModify 操做修改状态,该操做将输入参数传递给名为 modify 的 mutation 来改变状态。

首先,咱们能够给 store 里的每一个方法单独编写单元测试:

it('should modify state', () => {
  const state = {};

  store.mutations.modify(state, { info: 'Modified by click' });

  expect(state.info).to.eql('Modified by click');
});
复制代码

咱们也能够构建 store 来编写集成测试,从而检查总体是否能不抛出错误,正常运行:

import Vuex from 'vuex';
import { createLocalVue } from '@vue/test-utils';

it('should modify state', () => {
  const localVue = createLocalVue();
  localVue.use(Vuex);
  const vuexStore = new Vuex.Store(store);

  vuexStore.dispatch('onModify', 'Modified by click');

  expect(vuexStore.state.info).to.eql('Modified by click');
});
复制代码

首先,咱们必须建立一个 Vue 的局部实例,而后使用 use 语句。若是咱们不调用 use 方法,将会抛出一个错误。经过建立 Vue 的局部副本,咱们还能够避免污染全局对象。

咱们能够经过 dispatch 方法改变 store。第一个参数表示调用哪一个 action;第二个参数做为参数传递给 action。咱们能够随时经过 state 属性检查当前状态。

当使用组件的 store 时,咱们必须将局部 Vue 实例和 store 实例传递给 mount 函数。

const wrapper = shallowMount(Footer, { localVue, store: vuexStore });
复制代码

8. 路由

测试路由的设置与测试 store 有点相似,必须建立 Vue 实例的局部副本和路由实例,使用路由实例做为插件,而后建立组件。

<div class="route">{{ $router.path }}</div>
复制代码

上面这行组件模板将渲染当前路由路径。在测试中,咱们能够断言这个元素的内容。

import VueRouter from 'vue-router';
import { createLocalVue } from '@vue/test-utils';

it('should display route', () => {
  const localVue = createLocalVue();
  localVue.use(VueRouter);
  const router = new VueRouter({
    routes: [
      { path: '*', component: Footer }
    ]
  });

  const wrapper = shallowMount(Footer, { localVue, router });
  router.push('/modify');
  const text = wrapper.find('.route').text();

  expect(text).to.eql('/modify');
});
复制代码

咱们用 * 路径为组件添加了一个全匹配路由。有了 router 实例后,咱们还须要使用路由器的 push 方法为应用程序设置导航。

建立全部路由可能会是一项耗时的任务,咱们能够实现一个伪路由器,将其做为一个 mock 数据传递:

it('should display route', () => {
  const wrapper = shallowMount(Footer, {
    mocks: {
      $router: {
        path: '/modify'
      }
    }
  });
  const text = wrapper.find('.route').text();

  expect(text).to.eql('/modify');
});
复制代码

咱们也能够在 mocks 中定义一个 $store 属性来 mock store。

9. HTTP 请求

初始状态一般是经过 HTTP 请求获得的。咱们很容易在测试中完成真实的请求,但这会使得测试变得脆弱,而且对外部造成依赖。为了不这种状况,咱们能够在运行时更改请求的实现。在运行时更改实现称为 mocking,咱们将使用 Sinon 这一 mocking 框架来实现。

const store = {
  actions: {
    async onModify({ commit }, info) {
      const response = await axios.post('https://example.com/api', { info });
      commit('modify', { info: response.body });
    }
  }
};
复制代码

咱们在上面这段代码中修改了 store 的实现:首先输入参数经过 POST 请求被发送,而后将该请求获得的结果传递给 mutation。代码变成了异步,并有了一个外部依赖项,外部依赖项将是咱们在运行测试以前必须更改(mock)的项。

import chai from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);

it('should set info coming from endpoint', async () => {
  const commit = sinon.stub();
  sinon.stub(axios, 'post').resolves({
    body: 'Modified by post'
  });

  await store.actions.onModify({ commit }, 'Modified by click');

  expect(commit).to.have.been.calledWith('modify', { info: 'Modified by post' });
});
复制代码

咱们为 commit 方法建立了一个伪实现,并更改了 axios.post 的原始实现。这些伪实现能够捕获传递给它们的参数,并用咱们要求它们返回的内容进行响应。咱们没有为 commit 方法指定返回值,因此它将返回一个空值。axios.post 将返回一个 promise,该 promise 被解析为带有 body 属性的对象。

咱们必须将 sinonChai 做为一个插件添加到 Chai 中,以便可以对调用签名进行断言。这个插件扩展了 Chaito.have.been 属性和 to.have.been.calledWith 方法。

若是咱们返回一个 Promise,测试函数将变成异步的。Mocha 能够检测并等待异步函数完成。在函数内部,咱们等待 onModify 方法完成,而后断言伪 commit 方法是否被调用并传入了 post 调用返回的参数。

10. 浏览器

从代码的角度来看,咱们已经测试到了应用程序的各个方面。但有一个问题咱们仍然不能回答:应用程序能够在浏览器中运行吗?使用 Cypress 编写的端到端测试能够告诉咱们答案。

Vue CLI 提供以下功能:启动应用程序并在浏览器中运行 Cypress 测试,而后关闭应用程序。若是你想在 headless 模式下运行 Cypress 测试,你必须将 headless 标记添加到命令中。

describe('New todo', () => {
  it('it should change info', () => {
    cy.visit('/');

    cy.contains('.info', 'Click to modify');

    cy.get('button').click();

    cy.contains('.info', 'Modified by click');
  });
});
复制代码

上述测试代码的组织结构与单元测试相同:describe 表明测试分组,it 表明测试运行。全局变量 cy 表示 Cypress 运行器。咱们能够同步地命令运行程序在浏览器中执行什么操做。

在访问了主页(visit)以后,咱们能够经过 CSS 选择器访问页面中的 HTML。咱们可使用 contains 来断言元素的内容。页面交互也是相同的方式:首先,选择元素(get),而后进行交互(click)。在测试的最后,咱们检查内容是否更改。

总结

咱们已经介绍完了全部的测试用例,从一个函数的基本单元测试到在实际浏览器中运行的端到端测试。在本文中,咱们为 Vue 应用程序的构建块(组件、存储、路由)建立了集成测试,并介绍了 mocking 实现的一些基础。你能够在现有的或将来的项目中使用这些技术来避免程序上的 bug。但愿本文能下降你们为 Vue 应用程序编写测试的门槛。

本文中的示例阐明了测试相关的许多事情,但愿大家喜欢!


若是你以为这篇内容对你有价值,请点赞,并关注咱们的官网和咱们的微信公众号(WecTeam),每周都有优质文章推送:

WecTeam
相关文章
相关标签/搜索