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