← Back Home

前端单元测试

  1. JavaScript

为保证代码的质量,单元测试必不可少。本文记录自己在学习单元测试过程中的一些总结。

TDD与BDD的区别

TDD属于测试驱动开发,BDD属于行为驱动开发。个人理解其实就是TDD先写测试模块,再写主功能代码,然后能让测试模块通过测试,而BDD是先写主功能模块,z再写测试模块。详见示例

服务端代码测试

所谓服务端代码,指的就是一个node的模块,能在node的环境中运行。以一个项目为例,代码结构如下:

.
├── index.js
├── node_modules
├── package.json
└── test
    └── test.js

前端测试框架主要是MochaJasmine,这里我们选择Mocha,断言库有shouldexpectchai以及node自带的assert。这里我们选择chai,chai中包含了expect、should及assert的书写风格。

npm install mocha chai --save-dev
const getNum = (value) => {
  return value * 2
}

module.exports = getNum
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代表一个测试用例。

"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
window.createDiv = function(value) {
  var oDiv = document.createElement('div')
  oDiv.id = 'myDiv'
  oDiv.innerHTML = value
  document.body.appendChild(oDiv)
}
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()
<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文件便能看到测试结果 2018-01-11 2 15 12

当然我们可以选择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下执行,结果显示如图

2018-01-11 2 31 45

上述方式虽然能完成代码的单元测试,但是要完成代码覆盖率的计算也没有什么好的方式,所以我选择引入测试管理工具karma

karma使用指南

略去一大堆介绍karma的废话,项目下引入karma

npm install karma --save-dev

初始化配置karma配置文件

npm install karma -g
karma init

使用karma默认配置看看每一项的作用

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
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改为PhantomJSplugins中的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会加入许多其他的模块,对编译后的代码做测试覆盖率就没什么意义了。

const createDiv = value => {
  var oDiv = document.createElement('div')
  oDiv.id = 'myDiv'
  oDiv.innerHTML = value
  document.body.appendChild(oDiv)
}

module.exports = createDiv

我们使用ES6中的箭头函数

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")
  })
})
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。

2018-01-11 6 29 18

如果你是服务端代码使用istanbul计算代码覆盖率的

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 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/tmallfe.github.io/issues/37 https://codeutopia.net/blog/2015/03/01/unit-testing-tdd-and-bdd/ https://github.com/jdavis/tdd-vs-bdd https://jasmine.github.io/ https://github.com/bbraithwaite/karma-seed https://mochajs.org/ https://toutiao.io/posts/564973/app_preview https://coveralls.io/ https://karma-runner.github.io/0.13/index.html https://github.com/karma-runner/karma-coverage/blob/master/docs/configuration.md https://shouldjs.github.io/ https://juejin.im/post/59807358518825563e037e3c http://www.bradoncode.com/blog/2015/02/27/karma-tutorial/ http://docs.casperjs.org/en/latest/quickstart.html https://github.com/gotwarlost/istanbul https://www.jianshu.com/p/ffd6d319f05b http://www.bijishequ.com/detail/436708 http://phantomjs.org/ https://github.com/CurtisHumphrey/es6-library-boilerplate http://www.jackpu.com/shi-yong-babel-jspm-karma-jasmine-istanbul-shi-xian-es6-ce-shi-fu-gai-lu/ https://github.com/JackPu/JavaScript-Algorithm-Learning https://github.com/caitp/karma-coveralls https://github.com/karma-runner/karma-coverage

comments powered by Disqus