如何寫好eggjs單元測試
點(diǎn)擊上方 前端瓶子君,關(guān)注公眾號
回復(fù)算法,加入前端編程面試算法每日一題群

來源:bigo大魔王
https://juejin.cn/post/6949084159801294855
如何寫好eggjs單元測試
前言
筆者在平時(shí)面試前端同學(xué)時(shí),經(jīng)常遇到候選人有nodejs開發(fā)經(jīng)驗(yàn),但是很少有編寫單元測試。希望寫下這篇文章,讓大家多重視單元測試,交付高質(zhì)量的代碼。
如果你的項(xiàng)目單元測試分支規(guī)范率達(dá)到80%以上,我就認(rèn)為這個(gè)同學(xué)的代碼質(zhì)量意識特別好。
為什么要單元測試
如測試金字塔,單元測試是底座。
引用eggjs官網(wǎng)的話猛戳這里
你的代碼質(zhì)量如何度量? 你是如何保證代碼質(zhì)量? 你敢隨時(shí)重構(gòu)代碼嗎? 你是如何確保重構(gòu)的代碼依然保持正確性? 你是否有足夠信心在沒有測試的情況下隨時(shí)發(fā)布你的代碼?
如果答案都比較猶豫,那么就證明我們非常需要單元測試。
特別是大型nodejs項(xiàng)目,經(jīng)過多年的代碼迭代,業(yè)務(wù)邏輯復(fù)雜,代碼改動很容易牽一發(fā)動全身,單元測試就能給應(yīng)用的穩(wěn)定性提供了一層保障。不用面對qa的靈魂拷問:為什么老是你的bug最多!

測試準(zhǔn)備
eggjs提供了很好的測試模塊:egg-mock,通過egg-mock/bootstrap,可以快速實(shí)例化app
// test/controller/home.test.js
const { app, mock, assert } = require('egg-mock/bootstrap');
describe('test/controller/home.test.js', () => {
// test cases
});
復(fù)制代碼
自定義mockServiceByData與getMockData
但是我們知道,要寫單測,對mock數(shù)據(jù)比較依賴,需要我們準(zhǔn)備大量的json數(shù)據(jù),故在app.mockService基礎(chǔ)上拓展了mockServiceByData與getMockData方法
1.新建test/global.ts
注:如果是bigo內(nèi)網(wǎng),可以import bigoMock from '@bigo/bgegg-mock';
import * as assert from 'assert';
import { app } from 'egg-mock/bootstrap';
import * as path from 'path';
import * as fs from 'fs';
class BigoMock {
app;
ctx;
assert = assert; // 掛載assert
async before() {
console.log('hello bigoMock');
this.app = app;
await app.ready();
this.ctx = app.mockContext();
return;
}
/**
* 模擬 Service 方法返回值
* @param service 方法類
* @param methodName 方法名
* @param fileName 文件名(狀態(tài))
*/
mockServiceByData(service, methodName, fileName) {
let serviceClassName = '';
if (typeof service === 'string') {
const arr = service.split('.');
serviceClassName = arr[arr.length - 1];
}
const servicePaths = path.join(serviceClassName, methodName);
this.app.mockService(service, methodName, () => {
return this.getMockData(servicePaths, fileName);
});
}
/**
* 獲取本地test/mockData的mock數(shù)據(jù)
* @param folder 文件夾
* @param fileName 文件名
*/
getMockData(folder, fileName) {
return this.getJson(folder, fileName);
}
/**
* 約定從test/mockData/service/methodName/fileName.json獲取數(shù)據(jù)
* @param folder 文件夾
* @param fileName 文件名
*/
getJson(folder, fileName) {
// 默認(rèn)追加json后綴
console.log(path.extname(fileName));
if (!path.extname(fileName)) {
fileName = fileName + '.json';
}
const fullPaths = path.join(process.cwd(), 'test/mockData', folder, fileName);
return fs.readFileSync(fullPaths, 'utf-8');
}
}
const bigoMock = new BigoMock();
(async function() {
await bigoMock.before();
})();
export default bigoMock;
復(fù)制代碼
2.mockServiceByData
import bigoMock from './../global';
describe('user接口單測用例', () => {
it('should mock fengmk1 exists', () => {
// 返回test/mockData/user/get/success.json
bigoMock.mockServiceByData('user', 'get', 'success.json');
return app.httpRequest()
.get('/user?name=fengmk1')
.expect(200)
// 返回了原本不存在的用戶信息
.expect({
name: 'fengmk1',
});
});
});
復(fù)制代碼
3.getMockData
import bigoMock from './../global';
// TESTS=test/app/service/spider/githubIssues/index.test.ts npm test
describe('githubIssues爬蟲單測用例', () => {
it('解析html結(jié)構(gòu)成功', async () => {
// 返回test/mockData/githubIssues/html_mock.js
const html = bigoMock.getMockData('githubIssues', 'html_mock.js');
const result = bigoMock.ctx.service.spider.githubIssues.index.getLinks(html);
bigoMock.assert(result[0].title === 'nginx反向代理實(shí)現(xiàn)線上調(diào)試');
bigoMock.assert(result[0].href === 'https://github.com/bigo-frontend/blog/issues/3');
});
});
復(fù)制代碼
編寫Service單測
如果編寫controller單測,從用戶請求到達(dá) ==》 返回ctx.body數(shù)據(jù),這個(gè)過程會涉及Controller、Service、以及下游接口調(diào)用等環(huán)節(jié)。經(jīng)過的分支邏輯太多,數(shù)據(jù)會有很多中間狀態(tài),這樣要準(zhǔn)備的單測用例就特別復(fù)雜,導(dǎo)致單測分支覆蓋率低。但是Service就不一樣了,每個(gè)Service函數(shù)都是單一功能,有明確的輸入、輸出結(jié)果,只要我們的service單元測試代碼足夠多,單測覆蓋率自然就上去了。
當(dāng)然應(yīng)用的 Controller、Helper、Extend 等代碼,都必須也有對應(yīng)的單元測試保證代碼質(zhì)量。
綜上,本文會重點(diǎn)講service單測。
如何執(zhí)行單個(gè)測試文件
我們知道執(zhí)行 npm run test (實(shí)際執(zhí)行 egg-bin test),就會跑全部的測試用例,但是我們通常編寫單測時(shí),只關(guān)心當(dāng)前單測的執(zhí)行情況。我們可以在命令行執(zhí)行如下命令,執(zhí)行指定測試文件
TESTS=test/app/service/spider/githubIssues/index.test.ts npm test
復(fù)制代碼
如果我們一個(gè)單測文件的測試用例很多,只希望跑一個(gè)用例,可以使用it.only
import bigoMock from './../../../../global';
// TESTS=test/app/service/spider/githubIssues/index.test.ts npm test
describe('githubIssues爬蟲單測用例', () => {
it('解析html結(jié)構(gòu)成功', async () => {
const html = bigoMock.getMockData('githubIssues', 'html_mock.js');
const result = bigoMock.ctx.service.spider.githubIssues.index.getLinks(html);
bigoMock.assert(result[0].title === 'nginx反向代理實(shí)現(xiàn)線上調(diào)試');
bigoMock.assert(result[0].href === 'https://github.com/bigo-frontend/blog/issues/3');
});
// 只會執(zhí)行該用例
it.only('解析html結(jié)構(gòu)失敗', async () => {
const html = '';
const result = bigoMock.ctx.service.spider.githubIssues.index.getLinks(html);
bigoMock.assert(result.length === 0);
});
});
復(fù)制代碼
注:在提交代碼前,記得移除only,否則執(zhí)行npm run test時(shí),只會執(zhí)行該用例??
mock輸入
1.常量mock
一個(gè)service方法,通常有多個(gè)arguments,我們在調(diào)用service時(shí),可以簡單構(gòu)造入?yún)?/p>
// 只會執(zhí)行該用例
it.only('解析html結(jié)構(gòu)失敗', async () => {
const html = ''; // 常量mock
const result = bigoMock.ctx.service.spider.githubIssues.index.getLinks(html);
bigoMock.assert(result.length === 0);
});
復(fù)制代碼
2.文件mock
如果入?yún)ο筝^復(fù)雜,或者其他單測文件也可以復(fù)用,那么使用文件mock比較方便
it('解析html結(jié)構(gòu)成功', async () => {
const html = bigoMock.getMockData('githubIssues', 'html_mock.js'); // 文件mock
const result = bigoMock.ctx.service.spider.githubIssues.index.getLinks(html);
bigoMock.assert(result[0].title === 'nginx反向代理實(shí)現(xiàn)線上調(diào)試');
bigoMock.assert(result[0].href === 'https://github.com/bigo-frontend/blog/issues/3');
});
復(fù)制代碼
3.service依賴mock
假設(shè)service方法中,又調(diào)用了其他service方法,我們?yōu)榱私档透采w成本,通常會對該service依賴進(jìn)行mock。 譬如上面的爬蟲html解析后,需要進(jìn)行數(shù)據(jù)庫入庫的操作。this.service.githubIssues.create mock后,該方法不會被執(zhí)行, 直接返回create.json數(shù)據(jù),避免了測試數(shù)據(jù)入庫污染。
it('解析html結(jié)構(gòu)成功', async () => {
const html = bigoMock.getMockData('githubIssues', 'html_mock.js'); // 文件mock
bigoMock.app.mockService("githubIssues", "create", 'create.json'); // service依賴mock
const result = bigoMock.ctx.service.spider.githubIssues.index.getLinks(html);
bigoMock.assert(result[0].title === 'nginx反向代理實(shí)現(xiàn)線上調(diào)試');
bigoMock.assert(result[0].href === 'https://github.com/bigo-frontend/blog/issues/3');
});
復(fù)制代碼
4.上下文mock
如果我們想模擬 ctx.user 這個(gè)數(shù)據(jù),也可以通過給 mockContext 傳遞 data 參數(shù)實(shí)現(xiàn)。當(dāng)然,個(gè)人建議service減少上下文依賴,可以通過入?yún)⑦M(jìn)行數(shù)據(jù)傳遞,避免ctx.params.id這類寫法,讓代碼可測試。
it('should mock ctx.user', () => {
const ctx = app.mockContext({
user: {
name: 'fengmk2',
},
});
assert(ctx.user);
assert(ctx.user.name === 'fengmk2');
});
復(fù)制代碼
5.單測數(shù)據(jù)庫
也有人使用單測數(shù)據(jù)庫,在通過 before 和 after 方法,通在測試開頭創(chuàng)建數(shù)據(jù),結(jié)束的時(shí)候刪掉的。個(gè)人覺得成本較高,單元測試一般不依賴其他接口或者系統(tǒng),mock大法就好了。
當(dāng)然,實(shí)際的 Service 代碼不會像我們示例中那么簡單,這里只是展示如何測試 Service 而已。更多場景需要大家實(shí)戰(zhàn)補(bǔ)充。
結(jié)果斷言
這個(gè)沒有銀彈,通常要結(jié)合業(yè)務(wù)邏輯來編寫。
// 譬如
result = {
status: true,
data: {
age: '6',
name: 'bigo',
child: ['bigolive', 'likee'],
}
}
// 如果是判斷狀態(tài)值,只寫一個(gè)斷言就好
bigoMock.assert(result.status === true);
// 如果是部分?jǐn)?shù)據(jù)異常,就需要多個(gè)斷言組合一起
bigoMock.assert(result.status === true);
bigoMock.assert(result.data.name === 'bigo');
復(fù)制代碼
寫在最后
測試只是一種手段,而不是目的。
軟件的質(zhì)量不是測試出來的,而是設(shè)計(jì)和維護(hù)出來的。

延伸閱讀
更多細(xì)節(jié)請參考,eggjs.org/zh-cn/core/…
本文單元測試示例代碼來源于:github.com/bigo-fronte…
歡迎大家留言討論,祝工作順利、生活愉快!
