// 根據(jù) token 獲取用戶信息,必須登錄
router.get('/user', jwt, user.getSelf)
// 獲取用戶列表,無(wú)需登錄
router.get('/users', user.getList)
// 獲取指定用戶信息,無(wú)需登錄
router.get('/users/:userId', user.get)
// 創(chuàng)建新用戶(用戶注冊(cè)),無(wú)需登錄
router.post('/users', user.create)
// 更新用戶信息,必須登錄
router.put('/user', jwt, user.update)

那么我們應(yīng)該怎樣為這些 Restful API 編寫(xiě)單元測(cè)試呢?


  最基本流程是:
  · 為 app 創(chuàng)建 http 服務(wù)器
  · 對(duì)各個(gè) API 發(fā)出請(qǐng)求
  · 對(duì)響應(yīng)內(nèi)容進(jìn)行斷言
  幸運(yùn)的是,社區(qū)里已經(jīng)有相應(yīng)的工具讓我們可以方便管理這個(gè)流程,這個(gè)工具就是 —— supertest 。
  它提供了非常靈活的 API,足以幫助我們測(cè)試 Restful API 了。
  基本用法如下:
  

const app = require('../app')
  const request = require('supertest')(app)
  request
  .get('/users')
  .expect(200)
  .end((err, res) => {
  res.body.should.be.an.Array()
  })


  提示
  如果你遇到了 TypeError: app.address is not a function , 請(qǐng)嘗試一下以下方法:
  const request = require(‘supertest’).agent(app.listen())

現(xiàn)在,我們可以把 supertest 和其他測(cè)試框架整合起來(lái)了,我選擇了 mocha 作為例子,因?yàn)樗芙?jīng)典,當(dāng)你會(huì)用 mocha 之后,其他測(cè)試框架基本上就難不倒你了。

const co = require('co')
const { ObjectId } = require('mongoose').Types
const config = require('../config')
const UserModel = require('../models/user')
const app = require('../app')
const request = require('supertest')(app)
describe('User API', function (){


// 為每個(gè)單元測(cè)試初始化數(shù)據(jù)
// 每個(gè)單元測(cè)試中可以通過(guò) context 來(lái)訪問(wèn)相關(guān)的數(shù)據(jù)

beforeEach(function (done){
co(function* (){
self.user1 = yield UserModel.create({ username: 'user1' })
self.token = jwt.sign({ _id: self.user1._id }, config.jwtSecret, { expiresIn: 3600 })
done()
}).catch(err => {
console.log('err: ', err)
done()
})
})
// 正常情況下訪問(wèn) /user
it('should get user info when GET /user with token', function (done){
const self = this
request
.get('/user')
.set('Authorization', self.token)
.expect(200)
.end((err, res) => {
res.body._id.should.equal(self.user1._id)
done()
})
})
// 非正常情況下訪問(wèn) /user
it('should return 403 when GET /user without token', function (done){
request
.get('/user')
.expect(403, done)
})
// 訪問(wèn) /users,登錄用戶和非登錄用戶都會(huì)得到相同的結(jié)果,所以不需要區(qū)別對(duì)待
it('should return user list when GET /users', function (done){
request
.get('/users')
.expect(200)
.end((err, res) => {
res.body.should.be.an.Array()
done()
})
})
// 訪問(wèn) /users/:userId 也不需要區(qū)分登錄和非登錄狀態(tài)
it('should return user info when GET /users/:userId', function (done){
const self = this
request
.get(/users/${self.user1._id}) .expect(200) .end((err, res) => { res.body._id.should.equal(self.user1._id) done() }) }) // 訪問(wèn)不存在的用戶,我們需要構(gòu)造一個(gè)虛假的用戶 id it('should return 404 when GET /users/${non-existent}', function (done){ request .get(/users/${ObjectId()}) .expect(404, done) }) // 正常情況下的用戶注冊(cè)不會(huì)帶上 token it('should return user info when POST /user', function (done){ const username = 'test user' request .post('/users') .send({ username: username }) .expect(200) .end((err, res) => { res.body.username.should.equal(username) done() }) }) // 非法情況下的用戶注冊(cè),帶上了 token 的請(qǐng)求要判斷為非法請(qǐng)求 it('should return 400 when POST /user with token', function (done){ const username = 'test user 2' request .post('/users') .set('Authorization', this.token) .send({ username: username }) .expect(400, done) }) // 正常情況下更新用戶信息,需要帶上 token it('should return 200 when PUT /user with token', function (done){ request .put('/user') .set('Authorization', this.token) .send({ username: 'valid username' }) .expect(200, done) }) // 非法情況下更新用戶信息,如缺少 token it('should return 400 when PUT /user without token', function (done){ request .put('/user') .send({ username: 'valid username' }) .expect(400, done) }) })


  可以看到,為 Restful API 編寫(xiě)單元測(cè)試還有一個(gè)優(yōu)點(diǎn),就是可以輕易區(qū)分登錄狀態(tài)和非登錄狀態(tài)。如果要在用戶界面中測(cè)試這些功能,那么就需要不停地登錄和注銷,將會(huì)是一項(xiàng)累人的工作~
  另外,上面的例子中基本都是對(duì)返回狀態(tài)嗎進(jìn)行斷言的,你可以按照自己的需要進(jìn)行斷言。
  提示
  你可以選擇自己喜歡的斷言庫(kù),我這里選擇了 should.js,原因是好讀。
  個(gè)人認(rèn)為 should.js 和其他斷言庫(kù)比起來(lái)有個(gè)缺點(diǎn),就是不好寫(xiě)。
  value.should.xxx.yyy.zzz 這個(gè)形式和 assert.equal(value, expected) 相比不太直觀。
  另外由于 should.js 是通過(guò)擴(kuò)展 Object.prototype 的原型來(lái)實(shí)現(xiàn)的,但 null 值是一個(gè)例外,它不能訪問(wèn)任何屬性。
  因此 should.js 在 null 上會(huì)失效。
  一個(gè)變通的辦法是 (value === null).should.equal(true) 。

$ npm test
User api
should get user info when GET /user with token
should return 403 when GET /user without token
should return user list when GET /users
should return user info when GET /users/:userId
should return 404 when GET /users/${non-existent}
should return user info when POST /user
should return 400 when POST /user with token
should return 200 when PUT /user with token
should return 400 when PUT /user without token


  當(dāng)我們運(yùn)行測(cè)試時(shí),看到自己編寫(xiě)的測(cè)試都通過(guò)時(shí),心里都會(huì)非常踏實(shí)。
  而當(dāng)我們要對(duì)項(xiàng)目進(jìn)行重構(gòu)時(shí),這些測(cè)試用例會(huì)幫我們發(fā)現(xiàn)重構(gòu)過(guò)程中的問(wèn)題,減少 Debug 時(shí)間,提升重構(gòu)時(shí)的效率。

細(xì)節(jié)如何連接測(cè)試數(shù)據(jù)庫(kù)


  在 Node.js 的環(huán)境下,我們可以設(shè)置環(huán)境變量 NODE_ENV=test ,然后通過(guò)這個(gè)環(huán)境變量去連接測(cè)試數(shù)據(jù)庫(kù),這樣測(cè)試數(shù)據(jù)就不會(huì)存在于開(kāi)發(fā)環(huán)境下的數(shù)據(jù)庫(kù)拉!
  

// config.js
  module.exports = {
  development: {},
  production: {},
  test: {}
  }
  // app.js
  const ENV = process.NODE_ENV || 'development'
  const config = require('./config')[ENV]
  // connect db by config


  如何清空測(cè)試數(shù)據(jù)庫(kù)
  清空數(shù)據(jù)庫(kù)這種一次性的工作最好放到 npm scripts 中處理,需要進(jìn)行清空操作的時(shí)候直接運(yùn)行 npm run resetDB 就可以了。
  需要注意的是,編寫(xiě)清空數(shù)據(jù)庫(kù)腳本時(shí)必須判斷環(huán)境變量 NODE_ENV ,以免誤刪 production 環(huán)境下的數(shù)據(jù)。

// resetDB.js
const env = process.NODE_ENV || 'development'
if (env === 'test' || env === 'development') {
// connect db and delete data
} else {
throw new Error('You can not run this script in production.')
}
// package.json
{
"scripts": {
"resetDB": "node scripts/resetDB.js"
},
// ...
}

何時(shí)清空測(cè)試環(huán)境的數(shù)據(jù)庫(kù)


  如果是按照上面的原則來(lái)生成測(cè)試數(shù)據(jù)的話,測(cè)試數(shù)據(jù)其實(shí)可以不用刪掉的。
  但由于測(cè)試數(shù)據(jù)會(huì)占用我們的空間,最好還是把這些測(cè)試數(shù)據(jù)刪掉。
  那么,清空測(cè)試數(shù)據(jù)庫(kù)這個(gè)操作在測(cè)試前執(zhí)行好,還是測(cè)試后執(zhí)行好?
  我個(gè)人傾向于測(cè)試前刪除,因?yàn)橛袝r(shí)候我們需要進(jìn)入數(shù)據(jù)庫(kù),查看測(cè)試數(shù)據(jù)的正確性。
  如果在測(cè)試后清空測(cè)試數(shù)據(jù)庫(kù)的話,我們就沒(méi)辦法訪問(wèn)到測(cè)試數(shù)據(jù)了。

{
"scripts": {
"resetDB": "node scripts/resetDB.js",
"test": "NODE_ENV=test npm run resetDB && mocha --harmony"
},
// ...
}

本文章轉(zhuǎn)載微信公眾號(hào)@金陽(yáng)光測(cè)試

上一篇:

用ASP.NET Core 給你的API接口打造一個(gè)自定義認(rèn)證授體系

下一篇:

保護(hù)JavaScript客戶端到API服務(wù)的通信
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊(cè)

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場(chǎng)景實(shí)測(cè),選對(duì)API

#AI文本生成大模型API

對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對(duì)比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)