對於沒有接觸過單元測試的前端人員來説,想要系統的瞭解它,可能會比較困難,因為東西比較零散,會毫無頭緒。所以,我理了一下單元測試要用到的工具和需要知道的概念,幫助系統的理解。
什麼是單元測試
單元測試(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: