supertest 是一個短小精悍的接口測試工具,比如一個登錄接口的測試用例如下:
import request from 'supertest'
it('登錄成功', () => {
request('https://127.0.0.1:8080')
.post('/login')
.send({ username: 'HaiGuai', password: '123456' })
.expect(200)
})
整個用例感觀上非常簡潔易懂。這個庫挺小的,設計也不錯,還是 TJ Holowaychuk 寫的!今天就帶大家一起實現一個 supertest 的輪子吧,做一個測試框架!
思路
在寫代碼前,先根據上面的經典例子設計好整個框架。
還是從上面的例子可以看出:發送請求,處理請求,對結果進行 expect 這三步組成了整個框架的鏈路,組成一個用例的生命週期。
request -> process -> expect(200)
request 這一步可以由第三方 http 庫實現,比如 axios、node-fetch、superagent 都可以。
process 這一步就是業務代碼不需要理會,最後的 expect 則可以用到 Node.js 自己提供的 assert 庫來執行斷言語句。所以,我們要把精力放在如何執行這些斷言身上。
expect 最後一步是我們框架的整個核心,我們要做的是如何管理好所有的斷言,因爲開發者很有可能會像下面一樣多次執行斷言:
xxx
.expect(1 + 1, 2)
.expect(200)
.expect({ result: 'success'})
.expect((res) => console.log(res))
所以,我們需要一個數組 this._asserts = []
來存放這些斷言,然後再提供一個 end()
函數,用來最後一次性執行完這些斷言:
xxx
.expect(1 + 1, 2)
.expect(200)
.expect({ result: 'success'})
.expect((res) => console.log(res))
.end() // 把上面都執行了
有點像事件中心,只不過這裏每 expect
一下就相當於給 "expect" 這個事件加一個監聽器,最後 end
則類似觸發 "expect" 事件,把所有監聽器都執行。
我們還注意到一點 expect
函數有可能是用來檢查狀態碼 status
的,有的是檢查返回的 body
,還有些檢查 headers
的,因此每次調用 expect
函數除了要往 this._asserts
推入斷言回調,還要判斷所推入的斷言回調到底是給 headers
斷言、還是給 body
斷言或者給 status
斷言的。
將上面的思路整理出來,圖示如下:
其中我們只需要關注黃色和紅色部分即可。
簡單實現
剛剛說到“發送請求”這一步是可以由第三方庫完成的,這裏選用 superagent 作爲發送 npm 包,因爲這個庫的用法也是鏈式調用更符合我們的期望,舉個例子:
superagent
.post('/api/pet')
.send({ name: 'Manny', species: 'cat' }) // sends a JSON post body
.set('X-API-Key', 'foobar')
.set('accept', 'json')
.end((err, res) => {
// Calling the end function will send the request
});
這也太像了吧!這不禁給了我們一些靈感:基於 superagent,把上面的 expect
加到 superagent 裏,然後改寫一下 end
以及 restful 的 http 函數就 OK 了呀!“基於 XX,重寫方法和加自己的方法”,想到了什麼?繼承呀!superagent 恰好提供了 Request 這個類,我們只要繼承它再重寫方法和加 expect
函數就好了!
一個簡單 Request 子類實現如下(先不管怎麼區分斷言回調,只做一個簡單的 equals
作爲斷言回調):
import {Request} from 'superagent'
import assert from 'assert'
function Test(url, method, path) {
// 發送請求
Request.call(this, method.toUpperCase(), path)
this.url = url + path // 請求路徑
this._asserts = [] // Assertion 隊列
}
// 繼承 Request
Object.setPrototypeOf(Test.prototype, Request.prototype)
/**
* .expect(1 + 1, 2)
*/
Test.prototype.expect = function(a, b) {
this._asserts.push(this.equals.bind(this, a, b))
return this
}
// 判斷兩值是否相等
Test.prototype.equals = function(a, b) {
try {
assert.strictEqual(a, b)
} catch (err) {
return new Error(`我想要${a},但是你給了我${b}`)
}
}
// 執行所有 Assertion
Test.prototype.assert = function(err, res, fn) {
let errorObj = null
for (let i = 0; i < this._asserts.length; i++) {
errorObj = this._asserts[i](res)
}
fn(errorObj)
}
// 彙總所有 Assertion 結果
Test.prototype.end = function (fn) {
const self = this
const end = Request.prototype.end
end.call(this, function(err, res) {
self.assert(err, res, fn)
})
return this
}
上面繼承 Request 父類,提供了 expect
, equals
, assert
函數,並重寫了 end
函數,這僅僅是我們自己的 Test
類,最好向外提供一個 request
函數:
import methods from 'methods'
import http from 'http'
import Test from './Test'
function request(path) {
const obj = {}
methods.forEach(function(method) {
obj[method] = function(url) {
return new Test(path, method, url)
}
})
obj.del = obj.delete
return obj
}
methods 這個 npm 包會返回所有 restful 的函數名,如 post
, get
之類的。在新創建的對象裏添加這些 restful 函數,並通過傳入對應的 path
, method
和 url
創建 Test
對象,然後間接創建一個 http 請求,以此完成 “發送請求” 這一步。
然後可以這樣使用我們的框架了:
it('should be supported', function (done) {
const app = express();
let s;
app.get('/', function (req, res) {
res.send('hello');
});
s = app.listen(function () {
const url = 'http://localhost:' + s.address().port;
request(url)
.get('/')
.expect(1 + 1, 1)
.end(done);
});
});
創建一個服務器
上面 request
函數調用的時候會有個問題:我們每次都要在 app.listen
函數裏測試,那能不能在 request 的時候就傳入 app,然後直接發請求測試呢?比如:
it('should fire up the app on an ephemeral port', function (done) {
const app = express();
app.get('/', function (req, res) {
res.send('hey');
});
request(app)
.get('/')
.end(function (err, res) {
expect(res.status).toEqual(200)
expect(res.text).toEqual('hey')
done();
});
});
首先,我們在 request
函數裏檢測如果傳入的是 app 函數,那麼創建服務器。
function request(app) {
const obj = {}
if (typeof app === 'function') {
app = http.createServer(app) // 創建內部服務器
}
methods.forEach(function(method) {
obj[method] = function(url) {
return new Test(app, method, url)
}
})
obj.del = obj.delete
return obj
}
然後在 Test
類的 constructor 裏也可以獲取對應的 path,並監聽 0 號端口:
function Test(app, method, path) {
// 發送請求
Request.call(this, method.toUpperCase(), path)
this.redirects(0) // 禁止重定向
this.app = app // app/string
this.url = typeof app === 'string' ? app + path : this.serverAddress(app, path) // 請求路徑
this._asserts = [] // Assertion 隊列
}
// 通過 app 獲取請求路徑
Test.prototype.serverAddress = function(app, path) {
if (!app.address()) {
this._server = app.listen(0) // 內部 server
}
const port = app.address().port
const protocol = app instanceof https.Server ? 'https' : 'http'
return `${protocol}://127.0.0.1:${port}${path}`
}
最後,在 end
函數裏把剛剛創建的服務器關閉:
// 彙總所有 Assertion 結果
Test.prototype.end = function (fn) {
const self = this
const server = this._server
const end = Request.prototype.end
end.call(this, function(err, res) {
if (server && server._handle) return server.close(localAssert)
localAssert()
function localAssert() {
self.assert(err, res, fn)
}
})
return this
}
封裝報錯信息
再來看看我們是如何處理斷言的:斷言失敗會走到 catch 語句並返回一個 Error,最後返回 Error 傳入 end(fn)
的 fn
回調入參。但是這會有一個問題啊,我們看錯誤堆棧的時候就蒙逼了:
錯誤信息是符合預期的,但是錯誤堆棧就不太友好了:前三行會定位到我們自己的框架代碼裏!試想一下,如果別人用我們的庫 expect
出錯了,點了錯誤堆棧結果後,發現定位到了我們的源碼會不會覺得蒙逼?所以,我們要對 Error 的 err.stack
進行改造:
// 包裹原函數,提供更優雅的報錯堆棧
function wrapAssertFn(assertFn) {
// 保留最後 3 行
const savedStack = new Error().stack.split('\n').slice(3)
return function(res) {
const err = assertFn(res)
if (err instanceof Error && err.stack) {
// 去掉第 1 行
const badStack = err.stack.replace(err.message, '').split('\n').slice(1)
err.stack = [err.toString()]
.concat(savedStack)
.concat('--------')
.concat(badStack)
.join('\n')
}
return err
}
}
Test.prototype.expect = function(a, b) {
this._asserts.push(wrapAssertFn(this.equals.bind(this, a, b)))
return this
}
上面首先去掉當前調用棧前 3 行,也就是上面截圖的前 3 行,因爲這都屬於源碼裏的報錯,對開發者會有干擾,而後面的堆棧可以幫助開發者直接定位到那個涼了的 expect
了。當然,我們還把真實的源碼出錯地方作爲 badStack
也顯示出來,只是用 '------' 作爲分割了,最後的錯誤結果如下:
區分斷言回調
現在把注意力都放在 expect
這個最最核心的函數上,剛剛已用 equal
實現最簡單的斷言了,現在我們要添加對 headers
, status
和 body
的斷言,對它們的斷言函數的簡單實現如下:
import util from "util";
import assert from 'assert'
// 判斷當前狀態碼是否相等
Test.prototype._assertStatus = function(status, res) {
if (status !== res.status) {
const expectStatusContent = http.STATUS_CODES[status]
const actualStatusContent = http.STATUS_CODES[res.status]
return new Error('expected ' + status + ' "' + expectStatusContent + '", got ' + res.status + ' "' + actualStatusContent + '"')
}
}
// 判斷當前 body 是否相等
// 判斷當前 body 是否相等
Test.prototype._assertBody = function(body, res) {
const isRegExp = body instanceof RegExp
if (typeof body === 'object' && !isRegExp) { // 普通 body 的對比
try {
assert.deepStrictEqual(body, res.body)
} catch (err) {
const expectBody = util.inspect(body)
const actualBody = util.inspect(res.body)
return error('expected ' + expectBody + ' response body, got ' + actualBody, body, res.body);
}
} else if (body !== res.text) { // 普通文本內容的對比
const expectBody = util.inspect(body)
const actualBody = util.inspect(res.text)
if (isRegExp) {
if (!body.test(res.text)) { // body 是正則表達式的情況
return error('expected body ' + actualBody + ' to match ' + body, body, res.body);
}
} else {
return error(`expected ${expectBody} response body, got ${actualBody}`, body, res.body)
}
}
}
// 判斷當前 header 是否相等
Test.prototype._assertHeader = function(header, res) {
const field = header.name
const actualValue = res.header[field.toLowerCase()]
const expectValue = header.value
// field 不存在
if (typeof actualValue === 'undefined') {
return new Error('expected "' + field + '" header field');
}
// 相等的情況
if ((Array.isArray(actualValue) && actualValue.toString() === expectValue) || actualValue === expectValue) {
return
}
// 檢查正則的情況
if (expectValue instanceof RegExp) {
if (!expectValue.test(actualValue)) {
return new Error('expected "' + field + '" matching ' + expectValue + ', got "' + actualValue + '"')
}
} else {
return new Error('expected "' + field + '" of "' + expectValue + '", got "' + actualValue + '"')
}
}
// 優化錯誤展示內容
function error(msg, expected, actual) {
const err = new Error(msg)
err.expected = expected
err.actual = actual
err.showDiff = true
return err
}
然後在 expect
函數裏通過參數類型的判斷選擇對應的 _assertXXX
函數:
/**
* .expect(200)
* .expect(200, fn)
* .expect(200, body)
* .expect('Some body')
* .expect('Some body', fn)
* .expect('Content-Type', 'application/json')
* .expect('Content-Type', 'application/json', fn)
* .expect(fn)
*/
Test.prototype.expect = function(a, b, c) {
// 回調
if (typeof a === 'function') {
this._asserts.push(wrapAssertFn(a))
return this
}
if (typeof b === 'function') this.end(b)
if (typeof c === 'function') this.end(c)
// 狀態碼
if (typeof a === 'number') {
this._asserts.push(wrapAssertFn(this._assertStatus.bind(this, a)))
// body
if (typeof b !== 'function' && arguments.length > 1) {
this._asserts.push(wrapAssertFn(this._assertBody.bind(this, b)))
}
return this
}
// header
if (typeof b === 'string' || typeof b === 'number' || b instanceof RegExp) {
this._asserts.push(wrapAssertFn(this._assertHeader.bind(this, { name: '' + a, value: b })))
return this
}
// body
this._asserts.push(wrapAssertFn(this._assertBody.bind(this, a)))
return this
}
至此,我們完成基本的斷言功能了。
處理網絡錯誤
有時候會拋出的錯誤可能並不是因爲業務代碼出錯了,而是像網絡斷網這種異常情況。我們也要對這類錯誤進行處理,以更友好的方式展示給開發者,可以對 assert
函數進行改造:
// 執行所有 Assertion
Test.prototype.assert = function(resError, res, fn) {
// 通用網絡錯誤
const sysErrors = {
ECONNREFUSED: 'Connection refused',
ECONNRESET: 'Connection reset by peer',
EPIPE: 'Broken pipe',
ETIMEDOUT: 'Operation timed out'
};
let errorObj = null
// 處理返回的錯誤
if (!res && resError) {
if (resError instanceof Error && resError.syscall === 'connect' && sysErrors[resError.code]) {
errorObj = new Error(resError.code + ': ' + sysErrors[resError.code])
} else {
errorObj = resError
}
}
// 執行所有 Assertion
for (let i = 0; i < this._asserts.length && !errorObj; i++) {
errorObj = this._assertFunction(this._asserts[i], res)
}
// 處理 superagent 的錯誤
if (!errorObj && resError instanceof Error && (!res || resError.status !== res.status)) {
errorObj = resError
}
fn.call(this, errorObj || null, res)
}
至此,對於 status
, body
, headers
的斷言都實現了,並在 expect
裏合理使用這三者的斷言回調,同時還處理了網絡異常的情況。
Agent 代理
再來回顧一下我們是怎麼使用框架來寫測試用例的:
it('should handle redirects', function (done) {
const app = express();
app.get('/login', function (req, res) {
res.end('Login');
});
app.get('/', function (req, res) {
res.redirect('/login');
});
request(app)
.get('/')
.redirects(1)
.end(function (err, res) {
expect(res).toBeTruthy()
expect(res.status).toEqual(200)
expect(res.text).toEqual('Login')
done();
});
});
可以觀察到:每次調用 request
函數內部都會馬上創建一個服務器,調用 end
的時候又馬上關閉,連續測試的時候消耗很大而且完全可以公用一個 server。能不能對 A 系列的用例用 A_Server,而對 B 系列的用例用 B_Server 呢?
superagent 除了 Request 類,還提供強大的 Agent 類來解決這類的需求。參考剛剛寫的 Test
類,照貓畫虎寫一個自己的 TestAgent
類繼承原 Agent 類:
import http from 'http'
import methods from 'methods'
import {agent as Agent} from 'superagent'
import Test from './Test'
function TestAgent(app, options) {
// 普通函數調用 TestAgent(app, options)
if (!(this instanceof TestAgent)) {
return new TestAgent(app, options)
}
// 創建服務器
if (typeof app === 'function') {
app = http.createServer(app)
}
// https
if (options) {
this._ca = options.ca
this._key = options.key
this._cert = options.cert
}
// 使用 superagent 的代理
Agent.call(this)
this.app = app
}
// 繼承 Agent
Object.setPrototypeOf(TestAgent.prototype, Agent.prototype)
// host 函數
TestAgent.prototype.host = function(host) {
this._host = host
return this
}
// delete
TestAgent.prototype.del = TestAgent.prototype.delete
當然不要忘了把 restful 的方法也重載了:
// 重寫 http 的 restful method
methods.forEach(function(method) {
TestAgent.prototype[method] = function(url, fn) {
// 初始化請求
const req = new Test(this.app, method.toLowerCase(), url)
// https
req.ca(this._ca)
req.key(this._key)
req.cert(this._cert)
// host
if (this._host) {
req.set('host', this._host)
}
// http 返回時保存 Cookie
req.on('response', this._saveCookies.bind(this))
// 重定向除了保存 Cookie,同時附帶上 Cookie
req.on('redirect', this._saveCookies.bind(this))
req.on('redirect', this._attachCookies.bind(this))
// 本次請求就帶上 Cookie
this._attachCookies(req)
this._setDefaults(req)
return req
}
})
重寫的時候除了返回創建的 Test
對象,還對 https, host, cookie 做了一些處理。其實這些處理也不是我想出來的,是 superagent 裏的對它自己 Agent 類的處理,這裏就照抄過來而已 :)
使用 Class 繼承
上面都是用 prototype 來實現繼承,非常的蛋疼。這裏直接把代碼都改寫成 class 形式,同時整理 Test
和 TestAgent
兩個類的代碼:
// Test.js
import http from 'http'
import https from 'https'
import assert from 'assert'
import {Request} from 'superagent'
import util from 'util'
// 包裹原函數,提供更優雅的報錯堆棧
function wrapAssertFn(assertFn) {
// 保留最後 3 行
const savedStack = new Error().stack.split('\n').slice(3)
return function (res) {
const err = assertFn(res)
if (err instanceof Error && err.stack) {
// 去掉第 1 行
const badStack = err.stack.replace(err.message, '').split('\n').slice(1)
err.stack = [err.toString()]
.concat(savedStack)
.concat('--------')
.concat(badStack)
.join('\n')
}
return err
}
}
// 優化錯誤展示內容
function error(msg, expected, actual) {
const err = new Error(msg)
err.expected = expected
err.actual = actual
err.showDiff = true
return err
}
class Test extends Request {
// 初始化
constructor(app, method, path) {
super(method.toUpperCase(), path)
this.redirects(0) // 禁止重定向
this.app = app // app/string
this.url = typeof app === 'string' ? app + path : this.serverAddress(app, path) // 請求路徑
this._asserts = [] // Assertion 隊列
}
// 通過 app 獲取請求路徑
serverAddress(app, path) {
if (!app.address()) {
this._server = app.listen(0) // 內部 server
}
const port = app.address().port
const protocol = app instanceof https.Server ? 'https' : 'http'
return `${protocol}://127.0.0.1:${port}${path}`
}
/**
* .expect(200)
* .expect(200, fn)
* .expect(200, body)
* .expect('Some body')
* .expect('Some body', fn)
* .expect('Content-Type', 'application/json')
* .expect('Content-Type', 'application/json', fn)
* .expect(fn)
*/
expect(a, b, c) {
// 回調
if (typeof a === 'function') {
this._asserts.push(wrapAssertFn(a))
return this
}
if (typeof b === 'function') this.end(b)
if (typeof c === 'function') this.end(c)
// 狀態碼
if (typeof a === 'number') {
this._asserts.push(wrapAssertFn(this._assertStatus.bind(this, a)))
// body
if (typeof b !== 'function' && arguments.length > 1) {
this._asserts.push(wrapAssertFn(this._assertBody.bind(this, b)))
}
return this
}
// header
if (typeof b === 'string' || typeof b === 'number' || b instanceof RegExp) {
this._asserts.push(wrapAssertFn(this._assertHeader.bind(this, {name: '' + a, value: b})))
return this
}
// body
this._asserts.push(wrapAssertFn(this._assertBody.bind(this, a)))
return this
}
// 彙總所有 Assertion 結果
end(fn) {
const self = this
const server = this._server
const end = Request.prototype.end
end.call(this, function (err, res) {
if (server && server._handle) return server.close(localAssert)
localAssert()
function localAssert() {
self.assert(err, res, fn)
}
})
return this
}
// 執行所有 Assertion
assert(resError, res, fn) {
// 通用網絡錯誤
const sysErrors = {
ECONNREFUSED: 'Connection refused',
ECONNRESET: 'Connection reset by peer',
EPIPE: 'Broken pipe',
ETIMEDOUT: 'Operation timed out'
}
let errorObj = null
// 處理返回的錯誤
if (!res && resError) {
if (resError instanceof Error && resError.syscall === 'connect' && sysErrors[resError.code]) {
errorObj = new Error(resError.code + ': ' + sysErrors[resError.code])
} else {
errorObj = resError
}
}
// 執行所有 Assertion
for (let i = 0; i < this._asserts.length && !errorObj; i++) {
errorObj = this._assertFunction(this._asserts[i], res)
}
// 處理 superagent 的錯誤
if (!errorObj && resError instanceof Error && (!res || resError.status !== res.status)) {
errorObj = resError
}
fn.call(this, errorObj || null, res)
}
// 判斷當前狀態碼是否相等
_assertStatus(status, res) {
if (status !== res.status) {
const expectStatusContent = http.STATUS_CODES[status]
const actualStatusContent = http.STATUS_CODES[res.status]
return new Error('expected ' + status + ' "' + expectStatusContent + '", got ' + res.status + ' "' + actualStatusContent + '"')
}
}
// 判斷當前 body 是否相等
_assertBody(body, res) {
const isRegExp = body instanceof RegExp
if (typeof body === 'object' && !isRegExp) { // 普通 body 的對比
try {
assert.deepStrictEqual(body, res.body)
} catch (err) {
const expectBody = util.inspect(body)
const actualBody = util.inspect(res.body)
return error('expected ' + expectBody + ' response body, got ' + actualBody, body, res.body)
}
} else if (body !== res.text) { // 普通文本內容的對比
const expectBody = util.inspect(body)
const actualBody = util.inspect(res.text)
if (isRegExp) {
if (!body.test(res.text)) { // body 是正則表達式的情況
return error('expected body ' + actualBody + ' to match ' + body, body, res.body)
}
} else {
return error(`expected ${expectBody} response body, got ${actualBody}`, body, res.body)
}
}
}
// 判斷當前 header 是否相等
_assertHeader(header, res) {
const field = header.name
const actualValue = res.header[field.toLowerCase()]
const expectValue = header.value
// field 不存在
if (typeof actualValue === 'undefined') {
return new Error('expected "' + field + '" header field')
}
// 相等的情況
if ((Array.isArray(actualValue) && actualValue.toString() === expectValue) || actualValue === expectValue) {
return
}
// 檢查正則的情況
if (expectValue instanceof RegExp) {
if (!expectValue.test(actualValue)) {
return new Error('expected "' + field + '" matching ' + expectValue + ', got "' + actualValue + '"')
}
} else {
return new Error('expected "' + field + '" of "' + expectValue + '", got "' + actualValue + '"')
}
}
// 執行單個 Assertion
_assertFunction(fn, res) {
let err
try {
err = fn(res)
} catch (e) {
err = e
}
if (err instanceof Error) return err
}
}
export default Test
還有 TestAgent
import http from 'http'
import methods from 'methods'
import {agent as Agent} from 'superagent'
import Test from './Test'
class TestAgent extends Agent {
// 初始化
constructor(app, options) {
super()
// 創建服務器
if (typeof app === 'function') {
app = http.createServer(app)
}
// https
if (options) {
this._ca = options.ca
this._key = options.key
this._cert = options.cert
}
// 使用 superagent 的代理
Agent.call(this)
this.app = app
}
// host 函數
host(host) {
this._host = host
return this
}
// 重用 delete
del(...args) {
this.delete(args)
}
}
// 重寫 http 的 restful method
methods.forEach(function (method) {
TestAgent.prototype[method] = function (url, fn) {
// 初始化請求
const req = new Test(this.app, method.toLowerCase(), url)
// https
req.ca(this._ca)
req.key(this._key)
req.cert(this._cert)
// host
if (this._host) {
req.set('host', this._host)
}
// http 返回時保存 Cookie
req.on('response', this._saveCookies.bind(this))
// 重定向除了保存 Cookie,同時附帶上 Cookie
req.on('redirect', this._saveCookies.bind(this))
req.on('redirect', this._attachCookies.bind(this))
// 本次請求就帶上 Cookie
this._attachCookies(req)
this._setDefaults(req)
return req
}
})
export default TestAgent
最後再給大家看一下 request
函數的代碼:
import methods from 'methods'
import http from 'http'
import TestAgent from './TestAgent'
import Test from './Test'
function request(app) {
const obj = {}
if (typeof app === 'function') {
app = http.createServer(app)
}
methods.forEach(function(method) {
obj[method] = function(url) {
return new Test(app, method, url)
}
})
obj.del = obj.delete
return obj
}
request.agent = TestAgent
export default request
總結
至此,已經完美地實現了 supertest 這個庫啦,來總結一下我們都幹了什麼:
- 確定了
request -> process -> expect
的整體鏈路,expect 這一環是整個測試庫的核心 - 向外暴露
expect
函數用於收集斷言語句,以及end
函數用於批量執行斷言回調 - 在
expect
函數里根據入參要將_asssertStatus
或_assertBody
還是_assertHeaders
推入_asserts
數組裏 -
end
函數執行assert
函數來執行所有_asserts
裏所有的斷言回調,並對網絡錯誤也做了相應的處理 - 對拋出的錯誤 stack 也做了修改,更友好地展示錯誤
- 除了用
request
函數測試單個用例,也提供TestAgent
作爲 agent 測試一批的用例
最後
這是這期 “造輪子” 的最後一篇文章了,目前只出了 10 篇關於 “造輪子” 的文章。
雖然這系列的文章標題都是以 “造輪子” 爲開頭,但本質上是帶大家一步一步地閱讀源碼。相比於市面上 “精讀源碼” 的文章,這一系列的文章不會一上來就看源碼,而是從一個簡單需求開始,先實現一個最 Low 的代碼來解決問題,然後再慢慢地優化,最後進化成源碼的樣子。希望這樣可以由淺入深地帶大家看一遍源碼,同時又不會有太大的心理負擔 :)
爲什麼只寫 10 篇呢?一個原因是想嘗試一下別的領域了和看看書了。另一個原因是因爲每週都研究源碼,再從頭開始推演源碼的進化路程是十分消耗精力的,真的會累,怕後面會爛尾,就以現在最好的狀態收尾吧。
(完結散花🎉🎉)