前端单元测试
为保证代码的质量,单元测试必不可少。本文记录自己在学习单元测试过程中的一些总结。
TDD与BDD的区别
TDD属于测试驱动开发,BDD属于行为驱动开发。个人理解其实就是TDD先写测试模块,再写主功能代码,然后能让测试模块通过测试,而BDD是先写主功能模块,z再写测试模块。详见示例
服务端代码测试
所谓服务端代码,指的就是一个node的模块,能在node的环境中运行。以一个项目为例,代码结构如下:
. ├── index.js ├── node_modules ├── package.json └── test └── test.js
前端测试框架主要是Mocha与Jasmine,这里我们选择Mocha,断言库有should、expect、chai以及node自带的assert。这里我们选择chai,chai中包含了expect、should及assert的书写风格。
npm install mocha chai --save-dev
- index.js
const getNum = (value) => { return value * 2 } module.exports = getNum
- test.js
const chai = require('chai') const expect = chai.expect const getNum = require('../index') describe('Test', function() { it('should return 20 when the value is 10', function() { expect(getNum(10)).to.equal(20) }) })
describe用于给测试用例分组,it代表一个测试用例。
- package.json
"scripts": { "test": "mocha" }
需要在全局下安装Mocha
npm install mocha -g
项目目录下执行
npm run test
测试通过
完成代码测试之后我们再去看看代码测试的覆盖率。测试代码覆盖率我们选择使用istanbul,全局安装
npm install -g istanbul
使用istanbul启动Mocha
istanbul cover _mocha
测试通过,覆盖率100%
行覆盖率(line coverage):是否每一行都执行了?
函数覆盖率(function coverage):是否每个函数都调用了?
分支覆盖率(branch coverage):是否每个if代码块都执行了?
语句覆盖率(statement coverage):是否每个语句都执行了?
修改index.js再看代码覆盖率
const getNum = (value) => { if(value === 0) { return 1 }else { return value * 2 } } module.exports = getNum
发现代码覆盖率发生了变化
修改test.js添加测试用例
describe('Test', function() { it('should return 20 when the value is 10', function() { expect(getNum(10)).to.equal(20) }) it('should return 1 when the value is 0', function() { expect(getNum(0)).to.equal(0) }) })
代码覆盖率又回到了100%
客户端代码
客户端代码即运行在浏览器中的代码,代码中包含了window、document等对象,需要在浏览器环境下才能起作用。还是以一个项目为例,代码结构如下:
. ├── index.js ├── node_modules ├── package.json └── test └── test.js └── test.html
我们依然使用Mocha测试库及chai断言库。
npm install mocha chai --save-dev
- index.js
window.createDiv = function(value) { var oDiv = document.createElement('div') oDiv.id = 'myDiv' oDiv.innerHTML = value document.body.appendChild(oDiv) }
- test.js
mocha.ui('bdd') var expect = chai.expect describe("Tests", function () { before(function () { createDiv('test') }) it("content right", function () { var el = document.querySelector('#myDiv') expect(el).to.not.equal(null) expect(el.innerHTML).to.equal("test") }) }) mocha.run()
- test.html
<html> <head> <title> Tests </title> <link rel="stylesheet" href="../node_modules/mocha/mocha.css"/> </head> <body> <div id="mocha"></div> <script src="../node_modules/mocha/mocha.js"></script> <script src="../node_modules/chai/chai.js"></script> <script src="../index.js"></script> <script src="./test.js"></script> </body> </html>
直接用浏览器打开test.html文件便能看到测试结果
当然我们可以选择PhantomJS模拟浏览器去做测试,这里我们使用mocha-phantomjs对test.html做测试。
全局安装mocha-phantomjs
npm install mocha-phantomjs -g
修改package.json
"scripts": { "test": "mocha-phantomjs test/test.html" }
项目目录下执行
npm run test
mocha-phantomjs在mac下执行会报phantomjs terminated with signal SIGSEGV
,暂时没有找到什么解决方案。所以我在ubuntu下执行,结果显示如图
上述方式虽然能完成代码的单元测试,但是要完成代码覆盖率的计算也没有什么好的方式,所以我选择引入测试管理工具karma
karma使用指南
略去一大堆介绍karma的废话,项目下引入karma
npm install karma --save-dev
初始化配置karma配置文件
npm install karma -g karma init
使用karma默认配置看看每一项的作用
- Karma.conf.js
module.exports = function(config) { config.set({ basePath: '', // 设置根目录 frameworks: ['jasmine'], // 测试框架 files: [ // 浏览器中加载的文件 ], exclude: [ // 浏览器中加载的文件中排除的文件 ], preprocessors: { // 预处理 }, reporters: ['progress'], // 添加额外的插件 port: 9876, // 开启测试服务时监听的端口 colors: true, logLevel: config.LOG_INFO, autoWatch: true, // 监听文件变化,发生变化则重新编译 browsers: ['Chrome'], // 测试的浏览器 singleRun: false, // 执行测试用例后是否关闭测试服务 concurrency: Infinity }) }
此时的项目结构如下所示
. ├── index.js ├── node_modules ├── package.json ├── karma.conf.js └── test └── test.js
首先我们将测试框架jasmine改为我们熟悉的mocha及chai,添加files及plugins
npm install karma karma-mocha karma-chai mocha chai karma-chrome-launcher --save-dev
- karma.conf.js
module.exports = function(config) { config.set({ basePath: '', frameworks: ['mocha', 'chai'], files: [ 'index.js', 'test/*.js' ], exclude: [ ], preprocessors: { }, reporters: ['progress'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, concurrency: Infinity, plugins: [ 'karma-chrome-launcher', 'karma-mocha', 'karma-chai', ] }) }
全局安装karma-cli
npm install -g karma-cli
修改package.json
"scripts": { "test": "karma start karma.conf.js" }
执行npm run test便可以启用chrome去加载页面。
karma测试代码覆盖率
测试代码覆盖率,我们还是选择使用PhantomJS模拟浏览器,将singleRun
设为true
,即执行完测试用例就退出测试服务。
npm install karma-phantomjs-launcher --save-dev
将browsers
中的Chrome
改为PhantomJS
,plugins
中的karma-chrome-launcher
改为karma-phantomjs-launcher
执行npm run test ,测试通过且自动退出如图所示
引入karma代码覆盖率模块karma-coverage,改模块依赖于istanbul
npm install istanbul karma-coverage --save-dev
修改karma.conf.js
module.exports = function(config) { config.set({ basePath: '', frameworks: ['mocha', 'chai'], files: [ 'index.js', 'test/*.js' ], exclude: [ ], preprocessors: { 'index.js': ['coverage'] }, reporters: ['progress', 'coverage'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['PhantomJS'], singleRun: true, concurrency: Infinity, coverageReporter: { type : 'text-summary' }, plugins: [ 'karma-phantomjs-launcher', 'karma-mocha', 'karma-coverage', 'karma-chai', ] }) }
对index.js文件使用coverage进行预处理,加入karma-coverage插件,覆盖率测试输出coverageReporter配置,详见这里这里为了方便截图显示将其设为text-summary
执行npm run test,显示结果如下图所示
ES6代码覆盖率计算
目前的浏览器并不能兼容所有ES6代码,所以ES6代码都需要经过babel编译后才可在浏览器环境中运行,但编译后的代码webpack会加入许多其他的模块,对编译后的代码做测试覆盖率就没什么意义了。
- index.js
const createDiv = value => { var oDiv = document.createElement('div') oDiv.id = 'myDiv' oDiv.innerHTML = value document.body.appendChild(oDiv) } module.exports = createDiv
我们使用ES6中的箭头函数
- test.js
const createDiv = require('../index') describe("Tests", function () { before(function () { createDiv('test') }) it("content right", function () { var el = document.querySelector('#myDiv') expect(el).to.not.equal(null) expect(el.innerHTML).to.equal("test") }) })
- kama.conf.js
module.exports = function(config) { config.set({ basePath: '', frameworks: ['mocha', 'chai'], files: [ 'test/*.js' ], exclude: [ ], preprocessors: { 'index.js': ['coverage'], 'test/*.js': ['webpack'] }, reporters: ['progress', 'coverage'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['PhantomJS'], singleRun: true, concurrency: Infinity, coverageReporter: { type : 'text-summary' }, webpack: { module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { "presets": ["es2015"], "plugins": [["istanbul"]] } } } ] } }, plugins: [ 'karma-phantomjs-launcher', 'karma-mocha', 'karma-coverage', 'karma-webpack', 'karma-chai', ] }) }
test.js文件通过require引入index.js文件,所以files只需引入test.js文件,再对test.js做webpack预处理。
需要相关的babel,webpack,karma依赖如下:
"devDependencies": { "babel-core": "^6.26.0", "babel-loader": "^7.1.2", "babel-plugin-istanbul": "^4.1.5", "babel-preset-es2015": "^6.24.1", "chai": "^4.1.2", "istanbul": "^0.4.5", "karma": "^2.0.0", "karma-chai": "^0.1.0", "karma-coverage": "^1.1.1", "karma-mocha": "^1.3.0", "karma-phantomjs-launcher": "^1.0.4", "karma-webpack": "^2.0.9", "mocha": "^4.1.0", "webpack": "^3.10.0" }
执行npm run dev显示如图所示
travisCI及coveralls
travisCI的配置这里不做详解,我们将通过代码测试覆盖率上传到coveralls获取一个covarage的icon。
如果你是服务端代码使用istanbul计算代码覆盖率的
- .travis.yml
language: node_js node_js: - 'stable' - 8 branches: only: - master install: - npm install script: - npm test after_script: "npm install coveralls && cat ./coverage/lcov.info | coveralls"
如果你是服务端代码使用karma计算代码覆盖率的,则需使用coveralls模块
npm install coveralls karma-coveralls --save-dev
- Karma.conf.js
// Karma configuration module.exports = function(config) { config.set({ basePath: '', frameworks: ['mocha', 'chai'], files: [ 'test/*.js' ], exclude: [], preprocessors: { 'test/*.js': ['webpack'], 'index.js': ['coverage'] }, reporters: ['progress', 'coverage', 'coveralls'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['PhantomJS'], singleRun: true, concurrency: Infinity, webpack: { module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { "presets": ["es2015"], "plugins": [["istanbul"], ["transform-runtime"]] } } } ] } }, coverageReporter: { type : 'lcov', dir : 'coverage/' }, plugins: [ 'karma-webpack', 'karma-phantomjs-launcher', 'karma-coverage', 'karma-mocha', 'karma-chai', 'karma-coveralls' ], }) }
plugins添加karma-coveralls,reporters添加coveralls,coverageReporter输出配置改为lcov。
可以参考我自己实现的一个show-toast库
参考
https://github.com/tmallfe/tm...
https://codeutopia.net/blog/2...
https://github.com/jdavis/tdd...
https://jasmine.github.io/
https://github.com/bbraithwai...
https://mochajs.org/
https://toutiao.io/posts/5649...
https://coveralls.io/
https://karma-runner.github.i...
https://github.com/karma-runn...
https://shouldjs.github.io/
https://juejin.im/post/598073...
http://www.bradoncode.com/blo...
http://docs.casperjs.org/en/l...
https://github.com/gotwarlost...
https://www.jianshu.com/p/ffd...
http://www.bijishequ.com/deta...
http://phantomjs.org/
https://github.com/CurtisHump...
http://www.jackpu.com/shi-yon...
https://github.com/JackPu/Jav...
https://github.com/caitp/karm...
https://github.com/karma-runn...