# 测试后端应用
# 准备工作
更改 package.json
文件,添加 jest
依赖
"scripts": { | |
"start": "cross-env NODE_ENV=production node index.js", | |
"dev": "cross-env NODE_ENV=development nodemon index.js", | |
"test": "cross-env NODE_ENV=test jest --verbose --runInBand" | |
} |
这里添加了 jest
依赖,并添加了 test
脚本,用于运行测试用例。
使用 nodemon 的 npm run dev 脚本中指定应用的模式为 development。我们还指定默认的 npm start 命令将定义模式为 production。
需要安装 jest
依赖,并安装 cross-env
依赖,用于在 Windows 和 Linux/macOS 系统上运行测试用例。
npm install --save-dev cross-env | |
npm install --save-dev jest |
我们需要将将测试和生产环境分开,使用不同的数据库
在 config.js
添加
const MONGODB_URI = process.env.NODE_ENV === 'test' | |
? process.env.TEST_MONGODB_URI | |
: process.env.MONGODB_URI |
并更改 .env
,添加测试数据库的 uri
使用 supertest
包来编写测试 API 的测试
npm install --save-dev supertest |
注意输出 HTTP 请求的中间件阻碍了测试执行的输出,向 logger.js
中添加更改
const info = (...params) => { | |
if (process.env.NODE_ENV !== 'test') { | |
console.log(...params) | |
} | |
} | |
const error = (...params) => { | |
if (process.env.NODE_ENV !== 'test') { | |
console.error(...params) | |
} | |
} |
# 开始编写测试
这里以笔记应用为例
- 先创建一个
tests/test_helper.js
文件,用于封装一些重复的数据,如初始化数据库,读取数据库
const Note = require('../models/note') | |
const initialNotes = [ // 初始化数据库 | |
{ | |
content: 'HTML is easy', | |
important: false | |
}, | |
{ | |
content: 'Browser can execute only JavaScript', | |
important: true | |
} | |
] | |
const nonExistingId = async () => { // 返回 一个不存在的 id | |
const note = new Note({ content: 'willremovethissoon' }) | |
await note.save() | |
await note.remove() | |
return note._id.toString() | |
} | |
const notesInDb = async () => { // 返回 数据库中的 note | |
const notes = await Note.find({}) | |
return notes.map(note => note.toJSON()) | |
} | |
module.exports = { | |
initialNotes, | |
nonExistingId, | |
notesInDb | |
} |
- 创建一个
tests/note_api.test.js
文件,用于测试笔记应用其中结构是先beforeEach
初始化数据库,再test
测试,最后afterAll
关闭数据库连接
const mongoose = require('mongoose'); | |
const supertest = require('supertest'); | |
const { test, after, beforeEach } = require('@jest/globals') // 使用 Jest 的全局函数 | |
const assert = require('assert'); | |
const Note = require('../models/note'); | |
const helper = require('./test_helper') | |
const app = require('../app'); | |
const note = require('../models/note'); | |
const api = supertest(app); | |
// console.log(helper.initialNotes[0]); | |
beforeEach(async () => { // 在每个测试之前执行操作 | |
await Note.deleteMany({}); | |
for(let note of helper.initialNotes) { // 循环添加,这样就是同步操作 | |
let noteObject = new Note(note); | |
await noteObject.save(); | |
} | |
}) | |
describe('Note API tests', () => { | |
test('notes are returned as json', async () => { // 测试 API | |
await api | |
.get('/api/notes') | |
.expect(200) | |
.expect('Content-Type', /application\/json/); | |
}); | |
test('a valid note can be added', async () => { // 添加测试用例 | |
const newNote = { // 创建新的笔记 | |
content: 'async/await simplifies making async calls', | |
important: true, | |
} | |
await api // 发送 POST 请求 | |
.post('/api/notes') | |
.send(newNote) | |
.expect(201) | |
.expect('Content-Type', /application\/json/) | |
const notesAtEnd = await helper.notesInDb() // 获取数据库中的所有笔记 | |
assert.strictEqual(notesAtEnd.length, helper.initialNotes.length + 1) // 比较 | |
const contents = notesAtEnd.map(e => e.content) // 获取所有笔记的 content | |
assert(contents.includes('async/await simplifies making async calls')) // 包含 | |
}) | |
test('note without content is not added', async() => { // 测试不满足条件的是否添加 | |
const newNote = { | |
important: true | |
} | |
await api | |
.post('/api/notes') | |
.send(newNote) | |
.expect(400) | |
const notesAtEnd = await helper.notesInDb() | |
assert.strictEqual(notesAtEnd.length, helper.initialNotes.length) | |
}) | |
// 获取单个笔记测试 | |
test('a specific note can be viewed', async () => { | |
const notesAtStart = await helper.notesInDb(); // 从数据库获取所有笔记 | |
const noteToView = notesAtStart[0]; // 获取第一个笔记 | |
const resultNote = await api // 发送 GET 请求获取笔记 | |
.get(`/api/notes/${noteToView.id}`) | |
.expect(200) | |
.expect('Content-Type', /application\/json/); | |
assert.deepStrictEqual(resultNote.body, noteToView); // 比较 | |
}) | |
// 删除单个笔记测试 | |
test('a note can be deleted', async () => { | |
const notesAtStart = await helper.notesInDb(); // 删之前 | |
const noteToDelete = notesAtStart[0]; | |
await api | |
.delete(`/api/notes/${noteToDelete.id}`) | |
.expect(204); | |
const notesAtEnd = await helper.notesInDb(); // 删之后 | |
const contents = notesAtEnd.map(r => r.content) | |
assert(!contents.includes(noteToDelete.content)) // 不包含删除的笔记 | |
assert.strictEqual(notesAtEnd.length, notesAtStart.length - 1); | |
}) | |
}); | |
afterAll(async () => { | |
await mongoose.connection.close(); | |
}); |
- 在测试结束后就可以对
notes.js
中的promise
进行修改,改成async/await
const notesRouter = require('express').Router() | |
const Note = require('../models/note') | |
notesRouter.get('/', async (request, response) => { | |
const notes = await Note.find({}) | |
response.json(notes) | |
}) | |
notesRouter.get('/:id', async (request, response, next) => { | |
const note = await Note.findById(request.params.id) | |
if(note) { | |
response.json(note) | |
} else { | |
response.status(404).end() | |
} | |
}) | |
notesRouter.post('/', async (request, response, next) => { | |
const body = request.body | |
const note = new Note({ | |
content: body.content, | |
important: body.important || false, | |
}) | |
const savedNote = await note.save(); | |
response.status(201).json(savedNote); | |
}) | |
notesRouter.delete('/:id', async (request, response, next) => { | |
await Note.findByIdAndDelete(request.params.id) | |
response.status(204).end() | |
}) | |
notesRouter.put('/:id', (request, response, next) => { | |
const body = request.body | |
const note = { | |
content: body.content, | |
important: body.important, | |
} | |
Note.findByIdAndUpdate(request.params.id, note, { new: true }) | |
.then(updatedNote => { | |
response.json(updatedNote) | |
}) | |
.catch(error => next(error)) | |
}) | |
module.exports = notesRouter |
- 当修改为
async/awai
后,需要添加try...catch
来处理错误,因为async/await
是promise
的语法糖,所以try...catch
可以处理promise
的错误,可以通过往app.js
中添加require('express-async-errors')
来省略
安装库npm install express-async-errors
app.js
const config = require('./utils/config') | |
const express = require('express') | |
require('express-async-errors') // 添加对 async 函数的支持 | |
const app = express() | |
const cors = require('cors') | |
const notesRouter = require('./controllers/notes') | |
const middleware = require('./utils/middleware') | |
const logger = require('./utils/logger') | |
const mongoose = require('mongoose') | |
// ... |