Vue2总结(五)单元测试

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

趁着Vue3还没发布,先把vue2的各个知识点、源码、轮子全部温习一遍,今天的主题是Vue项目的单元测试重要性及其使用。
友情提示:阅读本文大概需要 30分钟

前言

当你的项目足够大的时候,在叠加模块和组件的过程中,是很有可能影响之前的模块。但是被影响的模块已经通过了测试,我们在迭代的时候,很少有测试人员会去重新测试这个系统。所以, 被影响的模块很可能就有了一个隐形的bug被部署到线上。因此我们采用自动化测试。最主要的作用是对于大型项目,在每次迭代的时候, 可以保证整个系统的正确运行, 确保系统的健壮。

Vue2总结(五)单元测试

单元测试的重要性:

  • 保证了研发质量

  • 提高项目的稳定性

  • 提高开发速度

单元测试

目前单元测试的使用方式有三种:

  • jest 或 mocha

  • @vue / test-utils

  • sinon

Vue CLI 拥有开箱即用的通过 Jest 或 Mocha 进行单元测试的内置选项。我们还有官方的 Vue Test Utils 提供更多详细的指引和自定义设置。Vue Test Utils 是 Vue.js 官方的单元测试实用工具库。

为什么要单元测试

作为一个程序员,单元测试或许是一个绕不开的坎。单元测试对提高代码质量很有帮助。因为,好的代码一般是便于测试的。如果在进行单元测试过程中发现自己的一些代码不方便进行测试,那么你可能需要重新审视这些代码,看是否有一些设计上不合理或者可以优化的地方。当然,这也并不是说代码应该“迁就”于单元测试,如果这样就有点儿本末倒置了。

总之,单元测试能提高程序的可靠性,让开发者在发布时更有底气,让使用者更有安全感。虽然编写单元测试需要花费一些时间,但相比于它所带来的优势,这些时间和精力上的花费还是值得的。另外值得注意的是,单元测试并不能完全代替功能测试,因为程序本身设计的逻辑错误或者其它的一些环境因素所造成的影响,单元测试可能无能为力。所以,单元测试只是保证你想让程序模块输出一只猪,它不会整出一头驴来。至于进一步的功能测试或者说“肉测”,仍然是有必要的。

vue-test-utils

根据官网介绍:vue-test-utils 是 Vue 生态圈中的一个开源项目,其前身是 avoriaz,avoriaz 也是一个不错的包,但其 README 中有说明,当 vue-test-utils 正式发布的时候, 它将会被废弃。vue-test-utils 能极大地简化 Vue.js 单元测试。例如,网上一搜 Vue 单元测试,得到的例子一般是像下面这样的(包括 vue-cli 提供的模板里默认也是这样):

import Vue from 'vue'
import HelloWorld from '@/components/HelloWorld'

describe('HelloWorld.vue'() => {
  it('should render correct contents'() => {
    const Constructor = Vue.extend(HelloWorld)
    const vm = new Constructor().$mount()
    expect(vm.$el.querySelector('.hello h1').textContent)
      .toEqual('Welcome to Your Vue.js App')
  })
})

使用 vue-test-utils 后,你可以像下面这样

import { shallow } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld'

describe('HelloWorld.vue'() => {
  it('should render correct contents'() => {
    const wrapper = shallow(HelloWorld, {
      attachToDocument: ture
    })

    expect(wrapper.find('.hello h1').text()).to.equal('Welcome to Your Vue.js App')
  })
})

可以看到代码更加简洁了。wrapper 内含许多有用的方法,上面的例子中所使用的 find() 其中最简单不过的一个。vue-test-utils 还有 createLocalVue() 等方法以及 stub 之类的功能,基本上可以完成绝大部分情况下的测试用例。此外还需要选择一个好用的断言库,通常是 chai,有时候结合 sinon 一起使用。chai 是一个优秀的库,里面的方法十分完善。不过个人并不太中意 chai 的语法,比如比较常用的

to.be.ok,to.not.be.ok,expect({a: 1, b: 2}).to.be.an('object').that.has.all.keys('a''b'),

为啥要那么长?为啥要那么多句点?顺序忘了怎么办?

所以一开始我就选择了 expect.js (expect 是 Jest 的一部分,可以单独安装使用),主要是它的语法更符合我的口味,这也为后期迁移到 Jest 省了不少事。一个合适测试框架 -- Jest。这里只提到了 Jest,当然也是个人喜好而已,这也是自己最终决定的方案。当然此前使用的 karma + mocha + chai + chrome… 那一套也有其适用场景和可取之处。后面将会提到 Jest 的一些优点和缺点。

单元测试之jest

首先新建一个demo

vue create demo-jest

这里采用全局安装的vue-cli3,记得勾选单元测试。项目demo创建好之后,你会发现多了一个 tests 文件夹,这里就是用来写单测的。其中还有一个 jest.config.js 的一个 jest 配置文件。

// jest.config.js

module.exports = {
  moduleFileExtensions: ["js""jsx""json""vue"],
  // 测试的文件类型

  transform: {
    "^.+\.vue$""vue-jest",
    ".+\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$":
      "jest-transform-stub",
    "^.+\.jsx?$""babel-jest"
  },
  // 类似webpack里面的loader

  transformIgnorePatterns: ["/node_modules/"],
  // 忽略node_modules里面的文件

  moduleNameMapper: {
    "^@/(.*)$""<rootDir>/src/$1"
  },
  // 快捷路径,可以使用@快速访问到 src 目录下的文件


  snapshotSerializers: ["jest-serializer-vue"],
  // 快照格式化

  testMatch: [
    "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"
  ],
  // 指定对哪些文件进行单元测试

  testURL: "http://localhost/",
  // 测试地址,模拟浏览器环境

  watchPlugins: [
    "jest-watch-typeahead/filename",
    "jest-watch-typeahead/testname"
  ]
};

在test文件夹下,默认给出了一个example.spec.js

// example.spec.js

import { shallowMount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";

// 定义一个测试集
describe("HelloWorld.vue", () => {
  // 第一个参数是一个名称,第二个是方法
  // 每一个 it 包裹一个单元测试的最小集
  it("renders props.msg when passed", () => {
    // 第一个参数是单元测试的名称或描述,第二个参数是函数
    // 下面的主体内容是校验
    const msg = "new message";
    const wrapper = shallowMount(HelloWorld, {
      // shallowMount 浅渲染 HelloWorld 
      // 如果使用渲染 可以写 mount(小写),上面的引入依赖对应变化
      propsData: { msg }
    });
    expect(wrapper.text()).toMatch(msg);
    // expect这里是断言,我们期望toMatch的值和msg的值是一样的
  });
});

这里我们再写一个计数器的组件来测试

// 新建一个 Counter.vue
<template>
  <div>
    <span>count: {{ count }}</span>
    <button @click = "count++">count++</button>
  </div>    
</template>

<script>
export default{
  data(){
    return{
      count: 0
    }
  }
}
</script>

<style></style>

// 新建一个单元测试 counter.spec.js
import { shallowMount } from "@vue/test-utils";
import Counter from "@/components/Counter.vue";

describe("HelloWorld.vue", () => {
  it("renders counter html", () => {
    const wrapper = shallowMount(Counter, {
      propsData: { msg }
    });
    expect(wrapper.html()).toMatchSnapshots();
    // 生成快照
  });
  if("count++", () => {
    const button = warpper.find("button");
    button.trigger("click");
    expect(warpper.vm.count).toBe(1);
  })
});

这里我们已经简单实现了点击计数的功能,在此基础上我们再额外增加别的功能。

// Counter.vue
<template>
  <div>
    <span>count: {{ count }}</span>
    <button @click = "handleClick">count++</button>
  </div>    
</template>

<script>
export default{
  data(){
    return{
      count: 0
    }
  },
  methods: {
    handleClick(){
      this.count++;
      this.$emit("change"this.count)
    }
  }
}
</script>

<style></style>


// counter.spec.js,这里安装一个叫 sinon 的库
import { shallowMount } from "@vue/test-utils";
import Counter from "@/components/Counter.vue";
import sinon from "sinon";

describe("HelloWorld.vue", () => {
  let isCalled = false;
  const change = sinon.spy();
  const wrapper = mount(Counter, {
    listener: {
      // change(){
      //   isCalled = true
      // }
      change
    }
  })

  it("renders counter html", () => {
    const wrapper = shallowMount(Counter, {
      propsData: { msg }
    });
    expect(wrapper.html()).toMatchSnapshots();
    // 生成快照
  });
  if("count++", () => {
    const button = warpper.find("button");
    button.trigger("click");
    expect(warpper.vm.count).toBe(1);
    // expect(isCalled).toBe(true);
    expect(change.isCall).toBe(true);
    button.trigger("click");
    expect(change.callCount).toBe(2);
  })
});

后续再业务中是否使用单元测试,以及分文件还是分组件的形式去书写我们的测试,具体看业务需求吧。


利用CI服务自动单测

现在已经有不少平台提供 CI 服务,例如 TravisCI 和 CircleCI。对于开源的项目,能免费使用这些平台的服务持续集成一些日常构建、测试工作。自己目前使用 CircleCI,具体原因就不多说了,使用哪个取决于自身喜好和具体业务情况,甚至可以考虑自己搭建 CI 服务器。


为自己的项目加入测试覆盖率徽标

在自己开源项目的 README 中加入一个显示单元测试覆盖率的徽标,会增进用户的第一印象。高覆盖率的徽标,会使项目显得更专业可靠,也能让用户进一步了解整个项目并最终选用。CodeCov 能提供这种服务,并可以结合前面提到的 CI 使用,通过 CI 在代码推送后自动执行单元测试,通过后将代码覆盖率相关数据发送给 CodeCov,这样,在 README 中加入的覆盖率徽标就能自动更新了。为此,你需要一个 codecov 账号(通常用 GitHub 账号登录即可)并安装 codecov 包。

$ yarn add -D codecov

然后在 CI 的任务配置里加入上传代码测试覆盖率数据的步骤,例如 CircleCI 配置如下:

steps:
    ## 前面部分任务省略

    # run tests!
    - run: yarn test

    # update codecov stats
    - run: ./node_modules/.bin/codecov

最后在 README 里加入微标图片就可以了。

Jest 的优缺点

前面提到了单元测试的重要性以及 Jest 的简单使用,这并非一时脑热,而是经过多次权衡和尝试之后才作的决定。主要是由于 Jest 相对于之前的方案有着不少优势,一些特性让测试变得更轻松愉快,更有效率。粗略总结如下:

优点
  • 一站式的解决方案

在使用 Jest 之前,我需要一个测试框架(mocha),需要一个测试运行器(karma),需要一个断言库(chai),需要一个用来做 spies/stubs/mocks 的工具(sinon 以及 sinon-chai 插件),一个用于测试的浏览器环境(可以是 Chrome 浏览器,也可以用 PhantomJS)。而使用 Jest 后,只要安装它,全都搞定了。

  • 全面的官方文档,易于学习和使用

Jest 的官方文档很完善,对着文档很快就能上手。而在之前,我需要学习好几个插件的用法,至少得知道 mocha 用处和原理吧 我得学会 karma 的配置和命令,chai 的各种断言方法……,经常得周旋于不同的文档站之间,其实是件很烦也很低效的事。

  • 配置简单方便

  • 更直观明确的测试信息提示

  • 方便的命令行工具

全局安装 Jest 后,可以在命令行执行单元测试,配合各种命令参数,可以方便地实现执行单个测试、监视文件变化并自动执行等功能。特别是对于监视文件变化并执行,它提供多种模式,可以只执行修改过的测试。

Jest 甚至提供了 jest-codemods 这一工具,用来将使用其它包的测试迁移为使用 Jest

缺点
  • jsdom 的一些局限性

因为 Jest 是基于 jsdom 的,jsdom 毕竟不是真实的浏览器环境,它在测试过程中其实并不真正的“渲染”组件。这会导致一些问题,例如,如果组件代码中有一些根据实际渲染后的属性值进行计算(比如元素的 clientWidth)就可能出问题,因为 jsdom 中这些参数通常默认是 0.所以有些情况下,测试中可能要施以一些骚操作,比如自行 mock(实例上就是伪造,但合理地伪造)一些中间值,来满足测试用例。如果你的项目中这样的情况很多,还是建议使用 karma + mocha + chrome 这一组合。

  • 周边相关的包可能还不完善

例如 vue-jest,目前的版本并不能完全实现 vue-loader 的功能。比如,使用 sass,postcss 之类的功能,它会抛出警告信息。代码中直接 import 实际的 css 文件,则有可能报错,这时则需要使用 mock 来模拟 css 文件。这些问题,在使用 karma-mocha Chrome 的时候是没有的,因为测试运行于真实的浏览器环境中。

ChromeHeadless vs. PhantomJS?

较新版本的 Chrome 支持以 headless 模式运行,这对于测试这种不需要显示界面的任务来说是很合适了(其实也可以使用常规模式,只不过执行测试的时候 Chrome 会弹出窗口)。而在 Chrome 推出 headless 模式功能之前。我们通常用 PhantomJS 的 headless WebKit 环境来进行测试,但它有着一些久未解决的问题,而且更新进度越来越慢。去年2018-03-05,因为开发组内部意见不合,PhantomJS 项目已经封存了代码暂停开发了。

Chrome headless 对于 PhantomJS 来说算是一个致命的打击,特别是 Chrome 官方推出的 puppeteer 在短时间内已经被广泛接受和使用。但其实 PhantomJS 还是有一些适用场景的,例如一些服务器并不支持 Chrome,这种情况下 PhantomJS 就有用武之地了。不过目前看来,对手的碾压以及自身维护团队的涣散,PhantomJS 显得越来越乏力。

总结

实践总是最有效率的学习方式,特别是对于前端这个领域上,每天都有新的东西出现。编写单元测试可能比较枯燥,因为它并不像做新功能一样让人兴奋。但只要耐心调试,当全部测试用例都通过,当最后测试覆盖率慢慢提升时,那种成就感也不亚于开发出了新功能,通过这也是成长为独当一面的大前端所必经之路。

最后

今天的 Vue2总结(五)单元测试 就分享到这里,我的公众号没有留言功能哈,有问题大家心里默念,我能感受到,谢谢 ~ 

原文始发于微信公众号(程序员思语):Vue2总结(五)单元测试