单元测试 & 代码覆盖率

优质
小牛编辑
137浏览
2023-12-01

新的一年

之前因为上家公司的经营出了问题,年前的大裁员,过了一个漫长的春节。
之后加入了新公司,然后正好赶上一个很紧急的项目,忙成狗,因此好久没更新文章了。
不过,我又回来啦!

前言

自动化测试,我们将使用karma和nightmare,内容会包括:

  1. 单元测试
  2. e2e测试(放下一篇文章)
    其实,单元测试一般用在写公共包的时候,比如通用的js函数库,通用的UI组件库。基本不太会在做业务项目的时候还使用单元测试。
    然后,e2e测试的话,那其实往往是测试工程师需要做的,往往使用selenium。
    那难道前端就不需要学测试了吗?
    答案肯定是否定的,不然我写个毛......
    vue的项目就用了单元测试和e2e。
    基于vue的UI组件库,比如:饿了么的element、滴滴的cube-ui、有赞的vant等都有单元测试(咩有e2e,因为没必要)。
    滴滴的话,我问了下黄轶大佬,他们项目前端自动化测试是用了单元测试和e2e的。
    总之,两种都是由应用场景的,e2e虽然用的不多,或者说有时候不是前端自动化的范畴,但是其实做起来很简单,学会准没错!

一、单元测试

安装karma

npm install karma --save-dev
npm install karma-jasmine karma-chrome-launcher jasmine-core --save-dev

package.jsonscripts配置
"test": "karma start"

初始化karma

$ karma init my.conf.js

Which testing framework do you want to use?
Press tab to list possible options. Enter to move to the next question.
> mocha

Do you want to use Require.js?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> no

Do you want to capture a browser automatically?
Press tab to list possible options. Enter empty string to move to the next question.
> Chrome
>

What is the location of your source and test files?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Press Enter to move to the next question.
> test/**/*.js
>

Should any of the files included by the previous patterns be excluded?
You can use glob patterns, eg. "**/*.swp".
Press Enter to move to the next question.
>

Do you want Karma to watch all the files and run the tests on change?
Press tab to list possible options.
> yes

初始成功以后,会生成一份karma.conf.js配置文件。

顺便,在根目录创建一个test的文件夹,在这文件夹中创建一份index.js,内容为:

describe('A spec suite', function() {
  it('contains a passing spec', function() {
    console.log('Hello Karma')
  })
})

运行一下:
npm run test
我们会看到程序会自动打开chrome浏览器,然后显示测试结果。

正式的写个单元测试

重新组织test文件夹

.
└── unit
    ├── index.js
    ├── karma.conf.js
    └── specs
        └── dom.spec.js

因为我们还要做e2e测试,所以,在test下,用各个文件夹区分,unit下就是单元测试的内容了。

安装一系列包

karma-webpack
karma-sourcemap-loader
karma-coverage
chai
sinon
sinon-chai
karma-sinon-chai
karma-mocha-reporter

karma-webpack:因为我们的项目代码是用es6或者es7写的,所以webpack编译是必须的
karma-sourcemap-loader:sourcemap明显是必要的
karma-coverage:做代码覆盖率的时候需要用
chai:搭配mocha断言
sinon:搭配mocha做spy、stub、mock
sinon-chai:用chai来做sinon的断言,可以说是扩展
karma-sinon-chai:方便在测试代码中的调用,直接就能用expect、sinon.spy等,不需要每个文件都import
karma-mocha-reporter:mocha测试完的报告

像karma、mocha、chai、sinon这种测试工具,我不会很详细的介绍,全部都介绍的话内容实在有点多,而且也比较枯燥。想要学习可以看我的参考资料,是我花了大把时间筛选整理出来的。

修改karma.conf.js

const webpackConfig = require('../../webpack.config.test.js')

module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',

    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['mocha', 'sinon-chai'],

    // list of files / patterns to load in the browser
    files: [
      './index.js'
    ],

    // list of files / patterns to exclude
    exclude: [
    ],

    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
      './index.js': ['webpack', 'sourcemap', 'coverage']
    },

    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['mocha', 'coverage'],

    coverageReporter: {
      dir: './coverage',
      reporters: [
        { type: 'lcov', subdir: '.' },
        { type: 'text-summary' }
      ]
    },
    .
    .
    .
    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: true,

    // Concurrency level
    // how many browser should be started simultaneous
    concurrency: Infinity,

    webpack: webpackConfig,
    webpackMiddleware: {
      stats: 'errors-only'
    }
  })
}

karma原本在根目录,我们直接移过来就好了。然后修改的不多,我稍微解释一下:

  1. files:将要被测试的文件
  2. preprocessors:在引入文件前,需要用什么方式处理,我们看到了,包括webpack、sourcemap、coverage
  3. reporters:测试完成后的报告,我们需要mocha的报告和coverage的报告
  4. coverageReporter:代码覆盖率生成的报告文件地址和存在形式设置
  5. webpack:在这需要引入webpack的配置,我们见到顶部,引入了webpack.test.config.js文件,我们待会儿会介绍里面的配置
  6. webpackMiddleware:stats: 'errors-only'我们让webpack的编译过程不显示出来,除非编译报错

配置webpack.test.config.js

const webpackConfigBase = require('./webpack.config.base.js')

const config = Object.assign(webpackConfigBase.config, {
  // sourcemap 模式
  devtool: '#inline-source-map',
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  },
})

module.exports = config
``

#### 编辑index.js入口文件
这个文件是为了配合`karma-webpack`的,详情见[Alternative Usage](https://github.com/webpack-contrib/karma-webpack#alternative-usage)
```js
// require all test files (files that ends with .spec.js)
// 语法说明:https://doc.webpack-china.org/guides/dependency-management/#require-context
const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)

// require all src files which in the app/common/js for coverage.
// you can also change this to match only the subset of files that
// you want coverage for.
const srcContext = require.context('../../app/common/js/', true, /\.js$/)
srcContext.keys().forEach(srcContext)

测试代码放在./specs文件夹下,被测试原文件在../../app/common/js/下。这里我只测试一些公共的js文件,如果你需要测试其它,可自行修改。比如一些基于vue的UI组件库,你想要测试所有组件代码,还需要做些配置上的修改,这方面可以参考滴滴的cube-ui项目,挺完整的,覆盖率也很高。

正式写测试代码

编辑dom.spec.js文件:

/**
 * 测试common/utils/dom.js
 */
import * as dom from 'common/js/utils/dom.js'

// const expect = require('chai').expect 装过sinon-chai就不需要这句了;sinon同理

describe('utils/dom', () => {
  // 测试hasClass
  it('hasClass', () => {
    const ele = document.createElement('div')
    ele.className = 'base kitty'
    // 是否含有base
    expect(dom.hasClass(ele, 'base')).to.be.equal(true)
    // 是否含有kitty
    expect(dom.hasClass(ele, 'kitty')).to.be.equal(true)
    // 是否含有tom
    expect(dom.hasClass(ele, 'tom')).to.be.equal(false)
    // 无参数
    expect(dom.hasClass()).to.be.equal(false)
  })
  // 测试addClass
  it('addClass', () => {
    const ele = document.createElement('div')
    ele.className = 'base'
    // 增加类名kitty
    dom.addClass(ele, 'kitty')
    expect(ele.className).to.be.equal('base kitty')
    // 再增加类名kitty,希望并不会有重复类名
    dom.addClass(ele, 'kitty')
    expect(ele.className).to.be.equal('base kitty')
  })
  // 测试removeClass
  it('removeClass', () => {
    const ele = document.createElement('div')
    ele.className = 'base kitty'
    // 删除类名kitty
    dom.removeClass(ele, 'kitty')
    expect(ele.className).to.be.equal('base')
    // 删除不存在的类名
    dom.removeClass(ele, 'tom')
    expect(ele.className).to.be.equal('base')
  })
  // 测试noce
  it('once', () => {
    const ele = document.createElement('div')
    const callback = sinon.spy()
    dom.once(ele, 'click', callback)
    // 点击一次
    ele.click()
    expect(callback).to.have.been.calledOnce
    // 再点击一次,预期应该是不调用callback的,所以依然为calledOnce
    ele.click()
    expect(callback).to.have.been.calledOnce
  })
})

代码注释已经很清楚啦~

运行测试

先修改下package.json配置:"test:unit": "karma start test/unit/karma.conf.js"
运行:

➜  construct git:(master) npm run test:unit

> vue-construct@1.0.0 test:unit /Users/Terry/WFE/vue-study/construct
> karma start test/unit/karma.conf.js


START:
ℹ 「wdm」:
ℹ 「wdm」: Compiled successfully.
ℹ 「wdm」: Compiling...
ℹ 「wdm」:
ℹ 「wdm」: Compiled successfully.
23 04 2018 01:25:39.438:INFO [karma]: Karma v2.0.0 server started at http://0.0.0.0:9876/
23 04 2018 01:25:39.440:INFO [launcher]: Launching browser Chrome with unlimited concurrency
23 04 2018 01:25:39.448:INFO [launcher]: Starting browser Chrome
23 04 2018 01:25:41.778:INFO [Chrome 66.0.3359 (Mac OS X 10.13.2)]: Connected on socket A9ZeKTNtnUU9MAceAAAA with id 51610088
  utils/dom
    ✔ hasClass
    ✔ addClass
    ✔ removeClass
    ✔ once

Finished in 0.008 secs / 0.004 secs @ 01:25:41 GMT+0800 (CST)

SUMMARY:
✔ 4 tests completed

=============================== Coverage summary ===============================
Statements   : 87.12% ( 142/163 ), 14 ignored
Branches     : 61.25% ( 49/80 ), 22 ignored
Functions    : 86.11% ( 31/36 ), 5 ignored
Lines        : 90.79% ( 138/152 )
================================================================================

参考资料

karma 测试框架的前世今生
karma thesis
karma 官网
前端自动化测试工具overview
前端自动化测试解决方案探析
mocha官网
代码测试覆盖率分析
聊一聊前端自动化测试
Sinon指南: 使用Mocks, Spies 和 Stubs编写JavaScript测试
sinon-chai github

论文是个很有意思的东西,看多了你会惊人地发现,很多大厂有深度的文章其实都是对论文的纯翻译~
另外还参考了vue和滴滴的cube-ui的项目测试部分。