国产gaysexchina男同gay,japanrcep老熟妇乱子伦视频,吃奶呻吟打开双腿做受动态图,成人色网站,国产av一区二区三区最新精品

Egg 單元測試

2020-02-06 14:11 更新

為什么要單元測試

先問我們自己以下幾個問題:

  • 你的代碼質(zhì)量如何度量?
  • 你是如何保證代碼質(zhì)量?
  • 你敢隨時重構(gòu)代碼嗎?
  • 你是如何確保重構(gòu)的代碼依然保持正確性?
  • 你是否有足夠信心在沒有測試的情況下隨時發(fā)布你的代碼?

如果答案都比較猶豫,那么就證明我們非常需要單元測試。

它能帶給我們很多保障:

  • 代碼質(zhì)量持續(xù)有保障
  • 重構(gòu)正確性保障
  • 增強自信心
  • 自動化運行

Web 應(yīng)用中的單元測試更加重要,在 Web 產(chǎn)品快速迭代的時期,每個測試用例都給應(yīng)用的穩(wěn)定性提供了一層保障。 API 升級,測試用例可以很好地檢查代碼是否向下兼容。 對于各種可能的輸入,一旦測試覆蓋,都能明確它的輸出。 代碼改動后,可以通過測試結(jié)果判斷代碼的改動是否影響已確定的結(jié)果。

所以,應(yīng)用的 Controller、Service、Helper、Extend 等代碼,都必須有對應(yīng)的單元測試保證代碼質(zhì)量。 當(dāng)然,框架和插件的每個功能改動和重構(gòu)都需要有相應(yīng)的單元測試,并且要求盡量做到修改的代碼能被 100% 覆蓋到。

測試框架

從 npm 搜索『test framework』, 我們會發(fā)現(xiàn)有大量測試框架存在,每個測試框架都有它的獨特之處。

Mocha

我們選擇和推薦大家使用 Mocha,功能非常豐富,支持運行在 Node.js 和瀏覽器中, 對異步測試支持非常友好。

Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases.

AVA

為什么沒有選擇最近比較火的 AVA,它看起來會跑得很快。 經(jīng)過我們幾個真實項目實踐下來,AVA 真的只是看起來很美,但是實際會讓測試代碼越來越難寫,成本越來越高。

@dead-horse 的評價:

AVA 自身不夠穩(wěn)定,并發(fā)運行文件多的時候會撐爆 CPU;如果設(shè)置控制并發(fā)參數(shù)的方式運行,會導(dǎo)致 only 模式無效。并發(fā)執(zhí)行對測試用例的要求很高,所有的測試不能有依賴,特別是遇到一些需要做 mock 的場景時,寫好很難。app 在初始化的時候是有耗時的,如果串行運行,只需要初始化一個 app 對它測試。 但是 AVA 每一個文件都運行在獨立進程,有多少個文件就需要初始化多少個 app。

@fool2fish 的評價:

如果是簡單的程序的話用 AVA 會快一些(但是本來就簡單可能也沒啥感覺), 如果是復(fù)雜的就不推薦了,比較大的問題是可能沒法給出準(zhǔn)確的錯誤堆棧, 另外并發(fā)可能會導(dǎo)致依賴的其他測試環(huán)境的服務(wù)掛掉,降低測試的成功率, 還有就是帶流程的測試(比如測試數(shù)據(jù)庫的增刪改查功能)真心不適合用 AVA。

斷言庫

同樣,測試斷言庫也是百花齊放的時代, 我們經(jīng)歷過 assert,到 should 和 expect,還是不斷地在嘗試更好的斷言庫。

直到我們發(fā)現(xiàn) power-assert, 因為『No API is the best API』, 最終我們重新回歸原始的 assert 作為默認(rèn)的斷言庫。

簡單地說,它的優(yōu)點是:

  • 沒有 API 就是最好的 API,不需要任何記憶,只需 assert 即可。
  • 強大的錯誤信息反饋
  • 強大的錯誤信息反饋
  • 強大的錯誤信息反饋

報錯信息實在太美太詳細,讓人有種想看錯誤報告的欲望:

測試約定

為了讓我們更多地關(guān)注測試用例本身如何編寫,而不是耗費時間在如何運行測試腳本等輔助工作上, 框架對單元測試做了一些基本約定。

測試目錄結(jié)構(gòu)

我們約定 test 目錄為存放所有測試腳本的目錄,測試所使用到的 fixtures 和相關(guān)輔助腳本都應(yīng)該放在此目錄下。

測試腳本文件統(tǒng)一按 ${filename}.test.js 命名,必須以 .test.js 作為文件后綴。

一個應(yīng)用的測試目錄示例:

test
├── controller
│   └── home.test.js
├── hello.test.js
└── service
└── user.test.js

測試運行工具

統(tǒng)一使用 egg-bin 來運行測試腳本, 自動將內(nèi)置的 Mocha、co-mocha、power-assert,nyc 等模塊組合引入到測試腳本中, 讓我們聚焦精力在編寫測試代碼上,而不是糾結(jié)選擇那些測試周邊工具和模塊。

只需要在 package.json 上配置好 scripts.test 即可。

{
"scripts": {
"test": "egg-bin test"
}
}

然后就可以按標(biāo)準(zhǔn)的 npm test 來運行測試了。

npm test

> unittest-example@ test /Users/mk2/git/github.com/eggjs/examples/unittest
> egg-bin test

test/hello.test.js
? should work

1 passing (10ms)

準(zhǔn)備測試

本文主要介紹如何編寫應(yīng)用的單元測試,關(guān)于框架和插件的單元測試請查看框架開發(fā)插件開發(fā)相關(guān)章節(jié)。

mock

正常來說,如果要完整手寫一個 app 創(chuàng)建和啟動代碼,還是需要寫一段初始化腳本的, 并且還需要在測試跑完之后做一些清理工作,如刪除臨時文件,銷毀 app。

常常還有模擬各種網(wǎng)絡(luò)異常,服務(wù)訪問異常等特殊情況。

所以我們單獨為框架抽取了一個測試 mock 輔助模塊:egg-mock, 有了它我們就可以非??焖俚鼐帉懸粋€ app 的單元測試,并且還能快速創(chuàng)建一個 ctx 來測試它的屬性、方法和 Service 等。

app

在測試運行之前,我們首先要創(chuàng)建應(yīng)用的一個 app 實例, 通過它來訪問需要被測試的 Controller、Middleware、Service 等應(yīng)用層代碼。

通過 egg-mock,結(jié)合 Mocha 的 before 鉤子就可以便捷地創(chuàng)建出一個 app 實例。

// test/controller/home.test.js
const assert = require('assert');
const mock = require('egg-mock');

describe('test/controller/home.test.js', () => {
let app;
before(() => {
// 創(chuàng)建當(dāng)前應(yīng)用的 app 實例
app = mock.app();
// 等待 app 啟動成功,才能執(zhí)行測試用例
return app.ready();
});
});

這樣我們就拿到了一個 app 的引用,接下來所有測試用例都會基于這個 app 進行。 更多關(guān)于創(chuàng)建 app 的信息請查看 mock.app(options) 文檔。

每一個測試文件都需要這樣創(chuàng)建一個 app 實例非常冗余,因此 egg-mock 提供了一個 bootstrap 文件,可以直接從它上面拿到我們所常用的實例:

// test/controller/home.test.js
const { app, mock, assert } = require('egg-mock/bootstrap');

describe('test/controller/home.test.js', () => {
// test cases
});

ctx

我們除了 app,還需要一種方式便捷地拿到 ctx,方便我們進行 Extend、Service、Helper 等測試。 而我們已經(jīng)通過上面的方式拿到了一個 app,結(jié)合 egg-mock 提供的 app.mockContext(options) 方法來快速創(chuàng)建一個 ctx 實例。

it('should get a ctx', () => {
const ctx = app.mockContext();
assert(ctx.method === 'GET');
assert(ctx.url === '/');
});

如果我們想模擬 ctx.user 這個數(shù)據(jù),也可以通過給 mockContext 傳遞 data 參數(shù)實現(xiàn):

it('should mock ctx.user', () => {
const ctx = app.mockContext({
user: {
name: 'fengmk2',
},
});
assert(ctx.user);
assert(ctx.user.name === 'fengmk2');
});

現(xiàn)在我們拿到了 app,也知道如何創(chuàng)建一個 ctx 了,那么就可以進行更多代碼的單元測試了。

測試執(zhí)行順序

特別需要注意的是執(zhí)行順序,盡量保證在執(zhí)行某個用例的時候執(zhí)行相關(guān)代碼。

常見的錯誤寫法

// Bad
const { app } = require('egg-mock/bootstrap');

describe('bad test', () => {
doSomethingBefore();

it('should redirect', () => {
return app.httpRequest()
.get('/')
.expect(302);
});
});

Mocha 剛開始運行的時候會載入所有用例,這時 describe 方法就會被調(diào)用,那 doSomethingBefore 就會啟動。 如果希望使用 only 的方式只執(zhí)行某個用例那段代碼還是會被執(zhí)行,這是非預(yù)期的。

正確的做法是將其放到 before 中,只有運行這個套件中某個用例才會執(zhí)行。

// Good
const { app } = require('egg-mock/bootstrap');

describe('good test', () => {
before(() => doSomethingBefore());

it('should redirect', () => {
return app.httpRequest()
.get('/')
.expect(302);
});
});

Mocha 使用 before/after/beforeEach/afterEach 來處理前置后置任務(wù),基本能處理所有問題。 每個用例會按 before -> beforeEach -> it -> afterEach -> after 的順序執(zhí)行,而且可以定義多個。

describe('egg test', () => {
before(() => console.log('order 1'));
before(() => console.log('order 2'));
after(() => console.log('order 6'));
beforeEach(() => console.log('order 3'));
afterEach(() => console.log('order 5'));
it('should worker', () => console.log('order 4'));
});

異步測試

egg-bin 支持測試異步調(diào)用,它支持多種寫法:

// 使用返回 Promise 的方式
it('should redirect', () => {
return app.httpRequest()
.get('/')
.expect(302);
});

// 使用 callback 的方式
it('should redirect', done => {
app.httpRequest()
.get('/')
.expect(302, done);
});

// 使用 async
it('should redirect', async () => {
await app.httpRequest()
.get('/')
.expect(302);
});

使用哪種寫法取決于不同應(yīng)用場景,如果遇到多個異步可以使用 async function,也可以拆分成多個測試用例。

Controller 測試

Controller 在整個應(yīng)用代碼里面屬于比較難測試的部分了,因為它跟 router 配置緊密相關(guān), 我們需要利用 app.httpRequest() SuperTest 發(fā)起一個真實請求, 來將 Router 和 Controller 連接起來,并且可以幫助我們發(fā)送各種滿足邊界條件的請求數(shù)據(jù), 以測試 Controller 的參數(shù)校驗完整性。 app.httpRequest() 是 egg-mock 封裝的 SuperTest 請求實例。

例如我們要給 app/controller/home.js:

// app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('homepage', '/', controller.home.index);
};

// app/controller/home.js
class HomeController extends Controller {
async index() {
this.ctx.body = 'hello world';
}
}

寫一個完整的單元測試,它的測試代碼 test/controller/home.test.js 如下:

const { app, mock, assert } = require('egg-mock/bootstrap');

describe('test/controller/home.test.js', () => {
describe('GET /', () => {
it('should status 200 and get the body', () => {
// 對 app 發(fā)起 `GET /` 請求
return app.httpRequest()
.get('/')
.expect(200) // 期望返回 status 200
.expect('hello world'); // 期望 body 是 hello world
});

it('should send multi requests', async () => {
// 使用 generator function 方式寫測試用例,可以在一個用例中串行發(fā)起多次請求
await app.httpRequest()
.get('/')
.expect(200) // 期望返回 status 200
.expect('hello world'); // 期望 body 是 hello world

// 再請求一次
const result = await app.httpRequest()
.get('/')
.expect(200)
.expect('hello world');

// 也可以這樣驗證
assert(result.status === 200);
});
});
});

通過基于 SuperTest 的 app.httpRequest() 可以輕松發(fā)起 GET、POST、PUT 等 HTTP 請求,并且它有非常豐富的請求數(shù)據(jù)構(gòu)造接口, 例如以 POST 方式發(fā)送一個 JSON 請求:

// app/controller/home.js
class HomeController extends Controller {
async post() {
this.ctx.body = this.ctx.request.body;
}
}

// test/controller/home.test.js
it('should status 200 and get the request body', () => {
// 模擬 CSRF token,下文會詳細說明
app.mockCsrf();
return app.httpRequest()
.post('/post')
.type('form')
.send({
foo: 'bar',
})
.expect(200)
.expect({
foo: 'bar',
});
});

更詳細的 HTTP 請求構(gòu)造方式,請查看 SuperTest 文檔。

mock CSRF

框架的默認(rèn)安全插件會自動開啟 CSRF 防護, 如果完整走 CSRF 校驗邏輯,那么測試代碼需要先請求一次頁面,通過解析 HTML 拿到 CSRF token, 然后再使用此 token 發(fā)起 POST 請求。

所以 egg-mock 對 app 增加了 app.mockCsrf() 方法來模擬取 CSRF token 的過程。 這樣在使用 SuperTest 請求 app 就會自動通過 CSRF 校驗。

app.mockCsrf();
return app.httpRequest()
.post('/post')
.type('form')
.send({
foo: 'bar',
})
.expect(200)
.expect({
foo: 'bar',
});

Service 測試

Service 相對于 Controller 來說,測試起來會更加簡單, 我們只需要先創(chuàng)建一個 ctx,然后通過 ctx.service.${serviceName} 拿到 Service 實例, 然后調(diào)用 Service 方法即可。

例如

// app/service/user.js
class UserService extends Service {
async get(name) {
return await userDatabase.get(name);
}
}

編寫單元測試:

describe('get()', () => {
it('should get exists user', async () => {
// 創(chuàng)建 ctx
const ctx = app.mockContext();
// 通過 ctx 訪問到 service.user
const user = await ctx.service.user.get('fengmk2');
assert(user);
assert(user.name === 'fengmk2');
});

it('should get null when user not exists', async () => {
const ctx = app.mockContext();
const user = await ctx.service.user.get('fengmk1');
assert(!user);
});
});

當(dāng)然,實際的 Service 代碼不會像我們示例中那么簡單,這里只是展示如何測試 Service 而已。

Extend 測試

應(yīng)用可以對 Application、Request、Response、Context 和 Helper 進行擴展。 我們可以對擴展的方法或者屬性針對性的編寫單元測試。

Application

egg-mock 創(chuàng)建 app 的時候,已經(jīng)將 Application 的擴展自動加載到 app 實例了, 直接使用這個 app 實例訪問擴展的屬性和方法即可進行測試。

例如 app/extend/application.js,我們給 app 增加了一個基于 ylru 的緩存功能:

const LRU = Symbol('Application#lru');
const LRUCache = require('ylru');
module.exports = {
get lru() {
if (!this[LRU]) {
this[LRU] = new LRUCache(1000);
}
return this[LRU];
},
};

對應(yīng)的單元測試:

describe('get lru', () => {
it('should get a lru and it work', () => {
// 設(shè)置緩存
app.lru.set('foo', 'bar');
// 讀取緩存
assert(app.lru.get('foo') === 'bar');
});
});

可以看到,測試 Application 的擴展是最容易的。

Context

Context 測試只比 Application 多了一個 app.mockContext() 步驟來模擬創(chuàng)建一個 Context 對象。

例如在 app/extend/context.js 中增加一個 isXHR 屬性,判斷是否通過 XMLHttpRequest 發(fā)起的請求:

module.exports = {
get isXHR() {
return this.get('X-Requested-With') === 'XMLHttpRequest';
},
};

對應(yīng)的單元測試:

describe('isXHR()', () => {
it('should true', () => {
const ctx = app.mockContext({
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
});
assert(ctx.isXHR === true);
});

it('should false', () => {
const ctx = app.mockContext({
headers: {
'X-Requested-With': 'SuperAgent',
},
});
assert(ctx.isXHR === false);
});
});

Request

通過 ctx.request 來訪問 Request 擴展的屬性和方法,直接即可進行測試。

例如在 app/extend/request.js 中增加一個 isChrome 屬性,判斷是否 Chrome 瀏覽器發(fā)起的請求:

const IS_CHROME = Symbol('Request#isChrome');
module.exports = {
get isChrome() {
if (!this[IS_CHROME]) {
const ua = this.get('User-Agent').toLowerCase();
this[IS_CHROME] = ua.includes('chrome/');
}
return this[IS_CHROME];
},
};

對應(yīng)的單元測試:

describe('isChrome()', () => {
it('should true', () => {
const ctx = app.mockContext({
headers: {
'User-Agent': 'Chrome/56.0.2924.51',
},
});
assert(ctx.request.isChrome === true);
});

it('should false', () => {
const ctx = app.mockContext({
headers: {
'User-Agent': 'FireFox/1',
},
});
assert(ctx.request.isChrome === false);
});
});

Response

Response 測試與 Request 完全一致。 通過 ctx.response 來訪問 Response 擴展的屬性和方法,直接即可進行測試。

例如在 app/extend/response.js 中增加一個 isSuccess 屬性,判斷當(dāng)前響應(yīng)狀態(tài)碼是否 200:

module.exports = {
get isSuccess() {
return this.status === 200;
},
};

對應(yīng)的單元測試:

describe('isSuccess()', () => {
it('should true', () => {
const ctx = app.mockContext();
ctx.status = 200;
assert(ctx.response.isSuccess === true);
});

it('should false', () => {
const ctx = app.mockContext();
ctx.status = 404;
assert(ctx.response.isSuccess === false);
});
});

Helper

Helper 測試方式與 Service 類似,也是通過 ctx 來訪問到 Helper,然后調(diào)用 Helper 方法測試。

例如 app/extend/helper.js

module.exports = {
money(val) {
const lang = this.ctx.get('Accept-Language');
if (lang.includes('zh-CN')) {
return `¥ ${val}`;
}
return `$ ${val}`;
},
};

對應(yīng)的單元測試:

describe('money()', () => {
it('should RMB', () => {
const ctx = app.mockContext({
// 模擬 ctx 的 headers
headers: {
'Accept-Language': 'zh-CN,zh;q=0.5',
},
});
assert(ctx.helper.money(100) === '¥ 100');
});

it('should US Dolar', () => {
const ctx = app.mockContext();
assert(ctx.helper.money(100) === '$ 100');
});
});

Mock 方法

egg-mock 除了上面介紹過的 app.mockContext() 和 app.mockCsrf() 方法外,還提供了非常多的 mock 方法幫助我們便捷地寫單元測試。

  • 如我們不想在終端 console 輸出任何日志,可以通過 mock.consoleLevel('NONE') 來模擬。
  • 又如我想模擬一次請求的 Session 數(shù)據(jù),可以通過 app.mockSession(data) 來模擬。describe('GET /session', () => { it('should mock session work', () => { app.mockSession({ foo: 'bar', uid: 123, }); return app.httpRequest() .get('/session') .expect(200) .expect({ session: { foo: 'bar', uid: 123, }, }); });});

因為 mock 之后會一直生效,我們需要避免每個單元測試用例之間是不能相互 mock 污染的, 所以通常我們都會在 afterEach 鉤子里面還原掉所有 mock。

describe('some test', () => {
// before hook

afterEach(mock.restore);

// it tests
});

引入 egg-mock/bootstrap 時,會自動在 afterEach 鉤子中還原所有的 mock,不需要在測試文件中再次編寫。

下面會詳細解釋一下 egg-mock 的常見使用場景。

Mock 屬性和方法

因為 egg-mock 是擴展自 mm 模塊, 它包含了 mm 的所有功能,這樣我們就可以非常方便地 mock 任意對象的屬性和方法了。

Mock 一個對象的屬性

mock app.config.baseDir 指向 /tmp/mockapp

mock(app.config, 'baseDir', '/tmp/mockapp');
assert(app.config.baseDir === '/tmp/mockapp');

Mock 一個對象的方法

mock fs.readFileSync 返回 hello world

mock(fs, 'readFileSync', filename => {
return 'hello world';
});
assert(fs.readFileSync('foo.txt') === 'hello world');

還有 mock.data(),mock.error() 等更多高級的 mock 方法, 詳細使用說明請查看 mm API。

Mock Service

Service 作為框架標(biāo)準(zhǔn)的內(nèi)置對象,我們提供了便捷的 app.mockService(service, methodName, fn) 模擬 Service 方法返回值。

例如,模擬 app/service/user 中的 get(name) 方法,讓它返回一個本來不存在的用戶數(shù)據(jù)。

it('should mock fengmk1 exists', () => {
app.mockService('user', 'get', () => {
return {
name: 'fengmk1',
};
});

return app.httpRequest()
.get('/user?name=fengmk1')
.expect(200)
// 返回了原本不存在的用戶信息
.expect({
name: 'fengmk1',
});
});

通過 app.mockServiceError(service, methodName, error) 可以模擬 Service 調(diào)用異常。

例如,模擬 app/service/user 中的 get(name) 方法調(diào)用異常:

it('should mock service error', () => {
app.mockServiceError('user', 'get', 'mock user service error');
return app.httpRequest()
.get('/user?name=fengmk2')
// service 異常,觸發(fā) 500 響應(yīng)
.expect(500)
.expect(/mock user service error/);
});

Mock HttpClient

框架內(nèi)置了 HttpClient,應(yīng)用發(fā)起的對外 HTTP 請求基本都是通過它來處理。 我們可以通過 app.mockHttpclient(url, method, data) 來 mock 掉 app.curl 和 ctx.curl 方法, 從而實現(xiàn)各種網(wǎng)絡(luò)異常情況。

例如在 app/controller/home.js 中發(fā)起了一個 curl 請求

class HomeController extends Controller {
async httpclient () {
const res = await this.ctx.curl('https://eggjs.org');
this.ctx.body = res.data.toString();
}
}

需要 mock 它的返回值:

describe('GET /httpclient', () => {
it('should mock httpclient response', () => {
app.mockHttpclient('https://eggjs.org', {
// 模擬的參數(shù),可以是 buffer / string / json,
// 都會轉(zhuǎn)換成 buffer
// 按照請求時的 options.dataType 來做對應(yīng)的轉(zhuǎn)換
data: 'mock eggjs.org response',
});
return app.httpRequest()
.get('/httpclient')
.expect('mock eggjs.org response');
});
});

示例代碼

完整示例代碼可以在 eggjs/exmaples/unittest 找到。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號