单元测试(unit testing):是指对软件中的最小可测试单元进行检查和验证。代码的终极目标有两个,第一个是实现需求,第二个是提高代码质量和可维护性。单元测试是为了提高代码质量和可维护性,是实现代码的第二个目标的一种方法。对vue组件的测试是希望组件行为符合我们的预期。

本文将从框架选型,环境搭建,使用方式,vue组件测试编写原则四个方面讲述如何在vue项目中落地单元测试。


一、框架选型

cypress / vue-test-utils

选择vue-test-utils 是因为它是官方推荐的vue component 单元测试库。

选择cypress而不是jest 主要是因为:

  • 测试环境的一致性: 在cypress上面跑的测试代码是在浏览器环境上的,而非像jest等在node上的。另外由于cypress在浏览器环境上运行,测试dom相关无需各种mock(如node-canvas等)
  • 统一测试代码风格、避免技术负担: 本身定位 e2e, 但是支持 unit test。
  • 支持CI环境

此外cypress还有很多非常棒的Features,感兴趣的朋友自行参考cypress官方文档。


二、环境搭建

1、安装依赖

npm i cypress @cypress/webpack-preprocessor start-server-and-test nyc babel-plugin-istanbul @vue/test-utils -D

note: 如果是使用vue cli3创建的项目,可以使用

# vue add @vue/cli-plugin-e2e-cypress
# npm i @cypress/webpack-preprocessor start-server-and-test nyc babel-plugin-istanbul @vue/test-utils -D

@cypress/webpack-preprocessor:引入webpack 预处理器

start-server-and-test:启动dev-server 监听端口启动成功,再执行测试命令。cypress 需要dev-server启动才能测试。

nyc babel-plugin-istanbul:覆盖率统计相关

2、添加/修改cypress.json文件

{"baseUrl": "http://localhost:9001","coverageFolder": "coverage","integrationFolder": "src","testFiles": "**/*.spec.js","video": false,"viewportHeight": 900,"viewportWidth": 1600,"chromeWebSecurity": false
}

3、修改package.json配置

"scripts": {"cy:run": "cypress run","cy:open": "cypress open","cy:dev": "start-server-and-test start :9001 cy:open","coverage": "nyc report -t=coverage","test": "rm -rf coverage && start-server-and-test start :9001 cy:run && nyc report -t=coverage"},

4、修改cypress/plugins/index.js(使用vue add @vue/cli-plugin-e2e-cypress的是tests/e2e//plugins/index.js)

// vue cli3 版本
const webpack = require('@cypress/webpack-preprocessor');
const webpackOptions = require('@vue/cli-service/webpack.config');webpackOptions.module.rules.forEach(rule => {if (!Array.isArray(rule.use)) return null;rule.use.forEach(opt => {if (opt.loader === 'babel-loader') {opt.options = {plugins: ['istanbul']};}});
});const options = {webpackOptions,watchOptions: {},
};module.exports = (on, config) => {on('file:preprocessor', webpack(options));return Object.assign({}, config, {integrationFolder: 'src',// screenshotsFolder: 'cypress/screenshots',// videosFolder: 'cypress/videos',// supportFile: 'cypress/support/index.js'})
};
// webpack4 版本const webpack = require('@cypress/webpack-preprocessor');
const config = require('../../webpack.base');
config.mode = 'development';
config.module.rules[0].use.options = {plugins: ['istanbul']
};module.exports = (on) => {const options = {// send in the options from your webpack.config.js, so it works the same// as your app's codewebpackOptions: config,watchOptions: {},};on('file:preprocessor', webpack(options));
};

5、修改cypress/support

// support/index.jsimport './commands';
import './istanbul';

在support目录里添加istanbul.js文件

// https://github.com/cypress-io/cypress/issues/346#issuecomment-365220178
// https://github.com/cypress-io/cypress/issues/346#issuecomment-368832585
/* eslint-disable */
const istanbul = require('istanbul-lib-coverage');const map = istanbul.createCoverageMap({});
const coverageFolder = Cypress.config('coverageFolder');
const coverageFile = `${ coverageFolder }/out-${Date.now()}.json`;Cypress.on('window:before:unload', e => {const coverage = e.currentTarget.__coverage__;if (coverage) {map.merge(coverage);}
});after(() => {cy.window().then(win => {const specWin = win.parent.document.querySelector('iframe[id~="Spec:"]').contentWindow;const unitCoverage = specWin.__coverage__;const coverage = win.__coverage__;if (unitCoverage) {map.merge(unitCoverage);}if (coverage) {map.merge(coverage);}cy.writeFile(coverageFile, JSON.stringify(map));cy.exec('npx nyc report --reporter=html -t=coverage')cy.exec('npm run coverage').then(coverage => {// output coverage reportconst out = coverage.stdout// 替换bash红色标识符.replace(/[31;1m/g, '').replace(/[0m/g, '')// 替换粗体标识符.replace(/[3[23];1m/g, '');console.log(out);}).then(() => {// output html file link to current test reportconst link = Cypress.spec.absolute.replace(Cypress.spec.relative, `${coverageFolder}/${Cypress.spec.relative}`).replace('cypress.spec.', '');console.log(`check coverage detail: file://${link}.html`);});});
});

6、修改package.json (推荐使用git push hooks 里跑test)

"gitHooks": {"pre-push": "npm run test"},"nyc": {"exclude": ["**/*.spec.js","cypress","example"]}

note: 如果项目使用了sass来写css,则必须指定node版本为v8.x.x,这个算是cypress的bug。Issuess

# npm install n -g
# sudo n v8.9.0
# npm rebuild node-sass

这样在git push之前会先跑单元测试,通过了才可以push成功。


三、使用方法

  • 对于各个 utils 内的方法以及 vue组件,只需在其目录下补充同名的 xxx.spec.js,即可为其添加单元测试用例。
  • 断言语法采用 cypress 断言: https://docs.cypress.io/guides/references/assertions.html#Chai
  • vue组件测试使用官方推荐的test-utils: https://vue-test-utils.vuejs.org/
  • npm 命令测试:
  • npm run cy:run (终端测试,前置条件:必须启动本地服务)
  • npm run cy:open (GUI 测试,前置条件:必须启动本地服务)
  • npm run cy:dev (GUI测试, 自动启动本地服务,成功后打开GUI)
  • npm run test (终端测试, 自动启动本地服务,并且统计覆盖率,在终端运行,也是CI运行的测试命令)

四、测试原则

1、明白要测试的是什么

不推荐一味追求行级覆盖率,因为它会导致我们过分关注组件的内部实现细节,而只关注其输入和输出。一个简单的测试用例将会断言一些输入 (用户的交互或 prop 的改变) 提供给某组件之后是否导致预期结果 (渲染结果或触发自定义事件)。

2、测试公共接口

a、如果模板有逻辑,我们应该测试它

// template
<button ref="logOutButton" v-if="loggedIn">Log out</button>
// Button.spec.jsconst PropsData = {loggedIn: true,
};it('hides the logOut button if user is not logged in', () => {const wrapper = mount(UserSettingsBar, { PropsData });const { vm } = wrapper;expect(vm.$refs.logOutButton).to.exist();wrapper.setProps({ loggedIn: false });expect(vm.$refs.logOutButton).not.to.exist();
});

原则:Props in Rendered Output

b、什么超出了我们组件的范围

  • 实现细节,过分关注组件的内部实现细节,从而导致琐碎的测试。
  • 测试框架本身, 这是vue应该去做的事情。
1、<p> {{ myProp }} </p>
expect(p.text()).to.be(/ prop value /);2、prop 校验   

c、权衡

Integration Test

// Count.spec.jsit('should display the updated count after button is clicked', () => {const wrapper = mount(Count, { count: 0});const ButtonInstance = wrapper.find(Button);const buttonEl = ButtonInstance.find('button')[0]; // find button clickbuttonE1.trigger('click');const CounterDisplayInstance = wrapper.find(CounterDisplay);const displayE1 = CounterDisplayInstance.find('.count-display')[0];expect(displayE1.text()).to.equal('1'); // find display, assert render
});

Shallow Test

// Count.spec.jsit('should pass the "count" prop to CounterDisplay', () => {const counterWrapper = shallow(Counter, {count: 10});const counterDisplayWrapper = counterWrapper.find(CounterDisplay);// we dont't care how this will be renderedexpect(counterDisplayWrapper.propsData().count).to.equal(10);
});it('should update the "count" prop by 1 on Button "increment" event', () => {const counterWrapper = shallow(Counter, {count: 10});const buttonWrapper = counterWrapper.find(Button);// we don't care how this was triggeredbuttonWrapper.vm.$emit('increment');expect(CounterDisplay.propsData().count).to.equal(11);
});


参考:

cypress-vue-unit-test

Vue Test Utils

Component Tests with Vue.js

vue 打开一个iframe_Vue 之五 —— 单元测试相关推荐

  1. vue项目点击左侧子菜单,打开一个新的浏览器标签页

    在项目开发中,产品给了这样一个需求:点击左侧子菜单,在浏览器中打开一个新的标签页,展示数据大屏.在此写个随笔记录下实现过程. 思路:使用编程式导航 实现页面跳转,我们常用的是 $router.push ...

  2. flask 检测post是否为空_用Flask和Vue制作一个单页应用(五)

    使用POST向后台发送数据 Flask app修改 可以直接使用现有的路由处理函数来接收前端发来的数据,server/app.py中的all_res()修改如下: @app.route('/resou ...

  3. 使用vue做一个“淘宝“项目——2

    做出首页 前言:做出底部导航栏与显示页面 目录: 创建项目文件 删除原有文件 引用资源文件 实现底部导航栏 显示页面 做出首页 显示商品栏 做出分类 一.将导航栏修改颜色 首先打开vant 2文档中的 ...

  4. vue实现一个类似浏览器搜索功能(ctrl + f)

    目录 引言 一.介绍自己项目的需求 二.先说说我的数据怎么设置的 三.具体功能的实现思路: 1.点击左侧目录跳转到对应位置 2.滚动到相应位置左侧目录树的对应标题变蓝色 3.搜索功能 4.目录展开和收 ...

  5. java计算机毕业设计vue开发一个简单音乐播放器(附源码、数据库)

    java计算机毕业设计vue开发一个简单音乐播放器(附源码.数据库) 项目运行 环境配置: Jdk1.8 + Tomcat8.5 + Mysql + HBuilderX(Webstorm也行)+ Ec ...

  6. vue打开新窗口并且实现传参,有图有真相

    我要实现的功能是打开一个新窗口用来展示新页面,而且需要传参数,并且参数不能显示在地址栏里面,而且当我刷新页面的时候,传过来的参数不能丢失,要一直存在,除非我手动关闭这个新窗口,即浏览器的标签页. 通过 ...

  7. vue 创建一个登录界面

    vue创建一个登录界面 (1)创建登录界面和主页 (2)配置路由 (3)配置main.js (4)配置App.vue (5)登录页面 (6)主页面 用到的组件 参考链接 (1)创建登录界面和主页 打开 ...

  8. 基于Vue实现一个简易的小程序框架,浅谈kafka | 每日掘金第 194 期

    Hello,又到了每天一次的下午茶时间.酱酱们的下午茶新增优质作者介绍和码上掘金板块,专注于发掘站内优质创作者和优质内容,欢迎大家多提宝贵意见! 酱酱们的下午茶全新改版,欢迎大家多提宝贵意见! 本文字 ...

  9. vue制作一个好看的网页

    1.安装并配置node.js (见本人博客-node.js) 2.建好的项目目录如下 build:  用来存放项目构建脚本 config: 存放项目的一些基本配置信息,最常用的就是端口转发 node_ ...

最新文章

  1. python使用imbalanced-learn的SMOTEN方法进行上采样处理数据不平衡问题
  2. 沟通CTBS助六和集团实现财务集中管理
  3. 内存迟迟下不去,可能你就差一个GC.Collect
  4. 10494,没过,待解决,大数除法
  5. python提取数组元素_python简单获取数组元素个数的方法
  6. 桂电计算机实训报告总结,桂林电子科技大学信息科技学院
  7. android openGL ES2 一切从绘制纹理開始
  8. ES7 设置磁盘使用率水位线 allocation.disk.watermark
  9. matlab的GUI实验——实现简单信号发生器
  10. 5、SpringBoot+MyBaits+Maven+Idea+pagehelper分页插件
  11. 如何搭建DASH直播平台
  12. html 点击按钮刷新验证码,HTML点击刷新验证码
  13. Delphi中使用Imageen控件将图像文件转换成PDF
  14. 计算机键盘中英文,电脑键盘中英文切换键
  15. 当路町-网络下载应用系列之二-破解网页内容无法复制
  16. 发现薪资被倒挂!跳槽还是等待?
  17. 计算机网络(4)传输层
  18. 安卓adb工具的使用
  19. go语言google pay支付验证订单
  20. android 京东收货地址,手机京东商城怎么添加收货地址?

热门文章

  1. 接受拒绝采样(Acceptance-Rejection Sampling)
  2. java collection详解_java 7 collection 详解(一)
  3. nginx 直接在配置文章中设置日志分割
  4. Unity优化之GC——合理优化Unity的GC (难度3 推荐5)
  5. ubuntu14.04不能安全卸载移动硬盘
  6. 1405 树的距离之和
  7. JavaScript GetAbsoultURl
  8. XML-RPC协议学习
  9. 不同类型的变量在内存中存储的详细情况
  10. python安装mysqlclient_Python-安装mysqlclient(MySQLdb)