# 测试后端应用

# 准备工作

更改 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)
  }
}

# 开始编写测试

这里以笔记应用为例

  1. 先创建一个 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
}
  1. 创建一个 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();
});
  1. 在测试结束后就可以对 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
  1. 当修改为 async/awai 后,需要添加 try...catch 来处理错误,因为 async/awaitpromise 的语法糖,所以 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')
// ...
更新于 阅读次数