造一個 supertest 輪子

文章源碼:https://github.com/Haixiang6123/my-supertest

參考輪子:https://www.npmjs.com/package/supertest

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 庫實現,比如 axiosnode-fetchsuperagent 都可以。

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, methodurl 創建 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, statusbody 的斷言,對它們的斷言函數的簡單實現如下:

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 形式,同時整理 TestTestAgent 兩個類的代碼:

// 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 這個庫啦,來總結一下我們都幹了什麼:

  1. 確定了 request -> process -> expect 的整體鏈路,expect 這一環是整個測試庫的核心
  2. 向外暴露 expect 函數用於收集斷言語句,以及 end 函數用於批量執行斷言回調
  3. expect 函數里根據入參要將 _asssertStatus_assertBody 還是 _assertHeaders 推入 _asserts 數組裏
  4. end 函數執行 assert 函數來執行所有 _asserts 裏所有的斷言回調,並對網絡錯誤也做了相應的處理
  5. 對拋出的錯誤 stack 也做了修改,更友好地展示錯誤
  6. 除了用 request 函數測試單個用例,也提供 TestAgent 作爲 agent 測試一批的用例

最後

這是這期 “造輪子” 的最後一篇文章了,目前只出了 10 篇關於 “造輪子” 的文章。

雖然這系列的文章標題都是以 “造輪子” 爲開頭,但本質上是帶大家一步一步地閱讀源碼。相比於市面上 “精讀源碼” 的文章,這一系列的文章不會一上來就看源碼,而是從一個簡單需求開始,先實現一個最 Low 的代碼來解決問題,然後再慢慢地優化,最後進化成源碼的樣子。希望這樣可以由淺入深地帶大家看一遍源碼,同時又不會有太大的心理負擔 :)

爲什麼只寫 10 篇呢?一個原因是想嘗試一下別的領域了和看看書了。另一個原因是因爲每週都研究源碼,再從頭開始推演源碼的進化路程是十分消耗精力的,真的會累,怕後面會爛尾,就以現在最好的狀態收尾吧。

(完結散花🎉🎉)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章