NodeJS Unit Test === ### 定義 :::success 以程式中最小的邏輯單元為對象,撰寫測試程式 ::: --- ### 撰寫風格 主要幾種測試腳本撰寫風格 - BDD (Behavior Driven Development) 行為驅動開發: 注重整體運作邏輯正確性。 - TDD (Test Driven Development) 測試功能導向開發: 注重功能結果正確性,先寫 test case 再開發。 - Others --- ### Tool - JavaScript單元測試工具 - 可在Node.js與瀏覽器上執行,實作前後端的測試腳本 #### Mocha (github star: 14,262) - 搭配多種斷言庫(Assertions) - should.js, expect.js, better-assert, unexpected - chai.js (should.js, expect.js, assert.js ) 三合一 - 資源較多 #### JEST (github star: 14,453) - 本次使用 - 較新穎 - 內建 coverage report, mock 等工具 --- ![](https://i.imgur.com/nAXKLVM.png) --- ### 安裝套件 ```shell= npm install --save-dev JEST ``` --- ### 指令&相關設定 * 關於JEST 基本設定 - 在`package.json`加入設定 - 再另外寫一個for Unit Test用的json/js檔 `jest.config.js/json`,再利用 `--config<path/to/js|json>`帶入設定 * for json type ```json= "jest": { "verbose": true, "globals": { "__DEV__": true }, "coverageReporters": [ "text", "html" ], "coverageDirectory": "unitTest/coverageReport" } ``` * for js type ```javascript= // jest.config.js module.exports = { verbose: true, globals: { __DEV__: true }, coverageReporters: [ 'text', 'html' ], coverageDirectory: 'unitTest/coverageReport' }; ``` * run script: ```JSON= "scripts": { "test": "jest --silent=false --colors --forceExit", "testReport": "jest --config --coverage --forceExit", } ``` * 若有使用ESLint 請在.eslintrc 設定檔內增加下列設定 ```JSON= { "env": { "jest/globals": true }, "plugins": [ "jest" ], "extends": "airbnb", "rules": { "jest/no-disabled-tests": "warn", "jest/no-focused-tests": "error", "jest/no-identical-title": "error", "jest/prefer-to-have-length": "warn", "jest/valid-expect": "error" } } ``` --- ### Getting Started * commonService.js file ```javascript= const uuidv1 = require('uuid/v1'); exports.generateUUIDByTimestamp = () => uuidv1(); exports.sum = (a, b) => (a + b); ``` * commonService.test.js file/ commonService.spec.js file ```javascript= const commonService = require('./commonService'); // after all test case end will run this function afterAll((done) => { setTimeout(() => { console.log('After all done'); server.close(); return done(); }, 3000); }); test('add 1 + 2 to equal 3', () => { expect(commonService.sum(1, 2)).toBe(3); }); describe('test UUID function', () => { // test case group test('generateUUIDByTimestamp', () => { expect(commonService.generateUUIDByTimestamp()).toHaveLength(36); }); test('generateUUIDByRandom', () => { expect(commonService.generateUUIDByRandom()).toHaveLength(36); }); test('generateUUIDByNameSpace', () => { const MY_NAMESPACE = commonService.generateUUIDByTimestamp(); expect(commonService.generateUUIDByNameSpace('Hello, World!', MY_NAMESPACE)).toHaveLength(36); }); }); ``` 檔案結構: ![](https://i.imgur.com/qEZWrNW.png) output: ![](https://i.imgur.com/hGhDQJb.png) ![](https://i.imgur.com/YhSdulk.png) --- ### coverage report #### at terminal ##### text-summery ```JSON= "coverageReporters": [ "text-summary", "html" ] ``` ![](https://i.imgur.com/u91ewdb.png) ##### text ```JSON= "coverageReporters": [ "text", "html" ] ``` ![](https://i.imgur.com/ncN9vCP.png) ---- #### coverage report (html) start at unitTest/coverageReport/index.html ![](https://i.imgur.com/wOew3Ga.png) :::info 可以看每一支檔案的覆蓋率及程式狀態 ( 是否有跑到 ) ::: ![](https://i.imgur.com/1iaDiCN.png) --- ### 延伸主題 #### 如何直接針對api 本身撰寫 intergration test case ? ( include testing async code ) * 安裝套件 ```shell= #整合測試api用 npm install --save-dev supertest ``` ```javascript= const request = require('supertest'); const app = require('../../app'); const server = require('../../bin/www'); describe('GET /devices/:device_id', () => { test('success request', (done) => { request(app) .get('/devices/device1') .set({ 'Content-Type': 'application/json', Authorization: 'JSON {"app_key":"11111-bfd6dac1-ddd9-4b57-b101-1d1a66a40616", "app_id":"16332180-b963-11e7-81c9-158522071c11"}'}) .then((response) => { expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty('device_did'); done(); }); }); test('not exist device', (done) => { request(app) .get('/devices/xxx') .set({ 'Content-Type': 'application/json', Authorization: 'JSON {"app_key":"11111-bfd6dac1-ddd9-4b57-b101-1d1a66a40616", "app_id":"16332180-b963-11e7-81c9-158522071c11"}' }) .then((response) => { expect(response.statusCode).toBe(400); // console.log(response.body); expect(response.body).toEqual(expect.any(Object)); expect(response.body).toEqual(expect.objectContaining({ time_used: expect.any(Number) })); expect(response.body.errorCode).toBe('INVALID_DEVICE_ID'); done(); }); }); }); ``` ---- #### Testing Asynchronous Code - Promises / async & await ```javascript= const config = require('config'); const commonService = require('./commonService'); const errorCode = require('../../errorCode/errorCode'); const envParams = config.get('unitTest').commonService; describe('sendVerifyCode case', () => { const params = { account_mobile: envParams.account_mobile, account_mobile_country_code: envParams.account_mobile_country_code, verify_code: envParams.verify_code }; // * promise test('sendVerifyCode success case', () => { expect.assertions(1); return commonService.sendVerifyCode(params) .then((result) => { expect(result).toEqual(expect.objectContaining({ account_mobile: expect.any(String), account_mobile_country_code: expect.any(String), verify_code: expect.any(String) })); }); }); test('sendVerifyCode fail case', () => { expect.assertions(2); return commonService.sendVerifyCode({}) .catch((error) => { console.log(error); expect(error).toEqual(expect.any(String)); expect(error).toBe(errorCode.VERIFY_CODE_SEND_FAILD); }); }); }); ``` * async await ```javascript= // * async await test('the data is peanut butter', async () => { expect.assertions(1); const data = await fetchData(); expect(data).toBe('peanut butter'); }); test('the fetch fails with an error', async () => { expect.assertions(1); try { await fetchData(); } catch (e) { expect(e).toMatch('error'); } }); ``` * async resolve intergration ```javascript= // * asnyc await resolve test('the data is peanut butter intergration', async () => { expect.assertions(1); await expect(fetchData()).resolves.toBe('peanut butter'); }); test('the fetch fails with an error intergration', async () => { expect.assertions(1); await expect(fetchData()).rejects.toMatch('error'); }); ``` --- ### Mock & more ... [Jest website](http://facebook.github.io/jest/docs/zh-Hans/api.html) --- ## Q & A