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