前端单元测试的基础内容

huanxueruxi 2019-07-01

对于没有接触过单元测试的前端人员来说,想要系统的了解它,可能会比较困难,因为东西比较零散,会毫无头绪。所以,我理了一下单元测试要用到的工具和需要知道的概念,帮助系统的理解。

什么是单元测试

单元测试(unit testing),顾名思义,是指对软件中的最小的可测试单元进行检查和验证。一个function、一个模块都是一个单元。一般来说,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为,所以倡导大家学会函数式编程。

为什么要做单元测试

单元测试从短期来看可能是很浪费时间,增加程序员负担的事,但是从长期来看,可以提高代码质量,减少维护成本,使代码得到了很好的回归,降低重构难度。
首先,我们可以保证代码的正确性,为上线做保障;
其次,可以做到自动化,一次编码,多次运行。比如,我们在项目中写了一个function,这个function在10个地方都被用到了,那么我们就不用在去10个地方都测试一遍,我们只要在单元测试的时候测试这个function就可以了;
然后,可阅读性强,一个项目由多人维护的时候,可以帮助后来维护的人快速看懂前一个人写的代码要实现什么功能;
还有,可以驱动开发,编写一个测试,它就定义了一个函数或一个函数的改进,开发人员就可以清楚地了解该特性的规范和要求。
最后,保证重构,随着互联网越来越快速的迭代,项目的重构也是很有必要的。对象、类、模块、变量和方法名应该清楚地表示它们当前的用途,因为添加了额外的功能,会导致很难去分辨命名的意义。

什么是TDD

上述有提到单元测试可以驱动开发,这就是TDD的功能。TDD,即Test-driven Development,翻译过来是测试驱动开发,就是在开发前,先编写单元测试用例,用于指导软件开发,使开发人员在编写代码之前关注需求。

断言

编写单元测试需要用到node.js的断言,这里就只列一下测试常用的,想要了解更多可以查看node.js文档

首先要引入node.js自带的断言assert:

const assert = require('assert');

1. assert.ok(value[, message])

value <any>
message <string> | <Error>

测试值是否为真,如果值不真实,则引发断言错误,并将消息属性设置为等于消息参数的值。如果消息参数未定义,则会分配默认错误消息。如果消息参数是一个错误的实例,那么它将被抛出,而不是断言错误。如果没有任何参数传入,则消息将被设置为字符串:没有值参数传递给assert.ok( )

assert.ok(true);
// OK
assert.ok(1);
// OK

assert.ok();
// AssertionError: No value argument passed to `assert.ok()`

assert.ok(false, 'it\'s false');
// AssertionError: it's false

2. assert.equal(actual, expected[, message])

actual <any>
expected <any>
message <string> | <Error>

测试实际参数和预期参数是否相等,这里的相等是==,不是===,会做隐式转换,如果传入message,报错内容为message里的内容

assert.equal(1, 1);
// OK, 1 == 1
assert.equal(1, '1');
// OK, 1 == '1'

assert.equal(1, 2);
// AssertionError: 1 == 2

assert.equal(1, 2, '1 should not equal 2');
// AssertionError [ERR_ASSERTION]: 1 should not equal 2

这里要注意的是引用类型的相等,equal判断的是指针地址是否相同,不会判断里面的值,从下面的例子可以看到

assert.equal({ a: { b: 1 } }, { a: { b: 1 } });
// AssertionError: { a: { b: 1 } } == { a: { b: 1 } }

// 空对象的引用地址不同,必不会相等
assert.equal({}, {})
// AssertionError [ERR_ASSERTION]: {} == {}

3. assert.strictEqual(actual, expected[, message])

actual <any>
expected <any>
message <string> | <Error>

测试实际参数和预期参数之间是否严格相等,与equal不同的是,strictEqual是===全等

assert.strictEqual(1, 1);
// OK

assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
// 1 !== 2

assert.strictEqual(1, '1');
// AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
// 1 !== '1'

assert.strictEqual(1, '1', '1 should not equal \'1\'');
// AssertionError [ERR_ASSERTION]: 1 should not equal '1'

4. assert.deepEqual(actual, expected[, message])

actual <any>
expected <any>
message <string> | <Error>

测试实际参数和预期参数之间的深度相等性,用 == 进行比较
与assert.equal( )比较,assert.deepequal( )可以实现不测试对象的[[原型]]或可枚举的自己的符号属性,即可以判断引用类型的值是否相等

const obj1 = {
  a: {
    b: 1
  }
};
const obj2 = {
  a: {
    b: 2
  }
};
const obj3 = {
  a: {
    b: 1
  }
};

const obj4 = Object.create(obj1);

assert.deepEqual(obj1, obj1);
// OK

// Values of b are different:
assert.deepEqual(obj1, obj2);
// AssertionError: { a: { b: 1 } } deepEqual { a: { b: 2 } }

assert.deepEqual(obj1, obj3);
// OK

// Prototypes are ignored:
assert.deepEqual(obj1, obj4);
// AssertionError: { a: { b: 1 } } deepEqual {}

5. assert.deepStrictEqual(actual, expected[, message])

actual <any>
expected <any>
message <string> | <Error>

assert.deepStrictEqual( )就很明显是判断引用类型的值是否全等

assert.deepStrictEqual(NaN, NaN);
// OK, because of the SameValue comparison

// Different unwrapped numbers:
assert.deepStrictEqual(new Number(1), new Number(2));
// AssertionError: Input A expected to strictly deep-equal input B:
// + expected - actual
// - [Number: 1]
// + [Number: 2]

assert.deepStrictEqual(new String('foo'), Object('foo'));
// OK because the object and the string are identical when unwrapped.

assert.deepStrictEqual(-0, -0);
// OK

// Different zeros using the SameValue Comparison:
assert.deepStrictEqual(0, -0);
// AssertionError: Input A expected to strictly deep-equal input B:
// + expected - actual
// - 0
// + -0

6. asser.throws(fn, error)

fn <Function>
error <RegExp> | <Function> | <Object> | <Error>
message <string>

捕获函数fn引发的错误并抛出。
如果指定error,错误可以是类、regexp、验证函数、验证对象(其中每个属性都将测试严格的深度相等),或者错误实例(其中每个属性都将测试严格的深度相等,包括不可枚举的消息和名称属性)。
当使用对象时,还可以使用正则表达式来验证字符串属性。
如果指定message,那么如果fn调用未能抛出或错误验证失败,message将附加到错误消息中。

// Error函数
assert.throws(
  () => {
    throw new Error('Wrong value');
  },
  Error
);
// 正则表达式
assert.throws(
  () => {
    throw new Error('Wrong value');
  },
  /^Error: Wrong value$/
);
const err = new TypeError('Wrong value');
err.code = 404;
err.foo = 'bar';
err.info = {
  nested: true,
  baz: 'text'
};
err.reg = /abc/i;

assert.throws(
  () => {
    throw err;
  },
  {
    name: 'TypeError',
    message: 'Wrong value',
    info: {
      nested: true,
      baz: 'text'
    }
  }
);

// Using regular expressions to validate error properties:
assert.throws(
  () => {
    throw err;
  },
  {
    name: /^TypeError$/,
    message: /Wrong/,
    foo: 'bar',
    info: {
      nested: true,
      baz: 'text'
    },
    reg: /abc/i
  }
);

// Fails due to the different `message` and `name` properties:
assert.throws(
  () => {
    const otherErr = new Error('Not found');
    otherErr.code = 404;
    throw otherErr;
  },
  err
);

mocha测试框架

mocha是JavaScript的一种单元测试框架,可以在node.js环境下运行,也可以在浏览器环境运行。
使用mocha,我们就只需要专注于编写单元测试本身,然后让mocha去自动运行所有的测试,并给出测试结果。mocha可以测试简单的JavaScript函数,也可以测试异步代码。

安装

npm install mocha -g

GET STARTED

编写代码:

const assert = require('assert');

describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return -1 when the value is not present', function() {
      assert.equal(-1, [1, 2, 3].indexOf(0))
    })
  })
})

在终端运行:

$ mocha

Array
    #indexOf()
      ✓ should return -1 when the value is not present
      
1 passing (9ms)

也可以在package.json里配置:

"scripts": {
    "test": "mocha"
}

然后在终端运行:

$ npm test

可以支持before、after、beforEach、afterEach来编码,通过给后面的匿名函数传入done参数来实现异步代码测试:

describe('should able to trigger an event', function () {
  var ele
  before(function () {
    ele = document.createElement('button')
    document.body.appendChild(ele)
  })

  it('should able trigger an event', function (done) {
    $(ele).on('click', function () {
      done()
    }).trigger('click')
  })

  after(function () {
    document.body.removeChild(ele)
    ele = null
  })
})

karma

karma是基础node.js的测试工具,主要测试于主流浏览器中,它可以监控文件的变化,然后自行执行,通过console.log显示测试结果。

安装

$ npm install karma-cli -g
$ npm install karma --save-dev

安装依赖(这里以mocha和chrome为例,如果要用firefox启动,就安装karma-firefox-launcher,要在浏览器中运行,所以要安装打开浏览器的插件):

$ npm install karma-chrome-launcher karma-mocha mocha --save-dev

初始化测试:

$ karma init

1. Which testing framework do you want to use ? (mocha)
2. Do you want to use Require.js ? (no)
3. Do you want to capture any browsers automatically ? (Chrome)
4. What is the location of your source and test files ? (test/**.js)
5. Should any of the files included by the previous patterns be excluded ? ()
6. Do you want Karma to watch all the files and run the tests on change ? (yes)

init后得到karma.conf.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'],

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

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

    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
    },

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

    // web server port
    port: 9876,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,

    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,

    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['Chrome'],

    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: false,

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

files表示要引用到的文件,如果有文件相互引用,不能用modules.exports暴露和require引入,会报错,只要把文件写入files就可以了。port是启动karma后,运行的端口,默认为9876。singleRun表示是否只运行一次,如果值为true,会默认执行一次后自动关闭浏览器,这样的话就不能做到实时监控文件了。

启动karma:

$ karma start

浏览器自动打开,还可以进行debug:

前端单元测试的基础内容

相关推荐