Test environment

Node 中的约定是用 NODE_ENV 环境变量定义应用的执行模式
修改package.json

  1. "scripts": {
  2. -- snip --
  3. "start": "NODE_ENV=production node index.js",
  4. "dev": "NODE_ENV=development nodemon index.js",
  5. "test": "NODE_ENV=test jest --verbose --runInBand"
  6. },

—runInBand防止Jest进行运行测试
在脚本中指定应用模式不能在windows上正常工作, 安装cross-env解决这个问题

  1. npm install --save-dev cross-env

再次修改scripts

  1. "scripts": {
  2. -- snip --
  3. "start": "cross-env NODE_ENV=production node index.js",
  4. "dev": "cross-env NODE_ENV=development nodemon index.js",
  5. "test": "cross-env NODE_ENV=test jest --verbose --runInBand",
  6. },

修改config.js

  1. require('dotenv').config()
  2. const PORT = process.env.PORT
  3. const MONGODB_URI = process.env.NODE_ENV === 'test'
  4. ? process.env.TEST_MONGODB_URI
  5. : process.env.MONGODB_URI
  6. module.exports = {
  7. MONGODB_URI,
  8. PORT
  9. }

修改.env文件, 添加note-app-test数据库

  1. MONGODB_URI=mongodb+srv://fullstack:secred@cluster0-ostce.mongodb.net/note-app?retryWrites=true
  2. PORT=3001
  3. TEST_MONGODB_URI=mongodb+srv://fullstack:secret@cluster0-ostce.mongodb.net/note-app-test?retryWrites=true

supertest

使用supertest包帮助我们编写API的测试
安装supertest开发依赖

  1. npm install --save-dev supertest

新建test/note_api.test.js

  1. const mongoose = require('mongoose')
  2. const supertest = require('supertest')
  3. const app = require('../app')
  4. const api = supertest(app)
  5. test('notes are returned as json', async () => {
  6. await api
  7. .get('/api/notes')
  8. .expect(200)
  9. .expect('Content-Type', /application\/json/)
  10. })
  11. afterAll(() => {
  12. mongoose.connection.close()
  13. })

如果测试报错,在项目根目录新建jest.config.js文件

  1. module.exports = {
  2. testEnvironment: 'node'
  3. }

或者给test添加第三个参数

  1. test('notes are returned as json', async () => {
  2. await api
  3. .get('/api/notes')
  4. .expect(200)
  5. .expect('Content-Type', /application\/json/)
  6. }, 100000)

再写一些测试

  1. test('there are two notes', async () => {
  2. const response = await api.get('/api/notes')
  3. expect(response.body).toHaveLength(2)
  4. })
  5. test('the first note is about HTTP methods', async () => {
  6. const response = await api.get('/api/notes')
  7. expect(response.body[0].content).toBe('HTML is easy')
  8. })

Initializing the database before tests

在每个test前使用beforeEach初始化数据库

  1. const mongoose = require('mongoose')
  2. const supertest = require('supertest')
  3. const app = require('../app')
  4. const api = supertest(app)
  5. const Note = require('../models/note')
  6. const initialNotes = [
  7. {
  8. content: 'HTML is easy',
  9. date: new Date(),
  10. important: false,
  11. },
  12. {
  13. content: 'Browser can execute only Javascript',
  14. date: new Date(),
  15. important: true
  16. }
  17. ]
  18. beforeEach(async () => {
  19. await Note.deleteMany({})
  20. let noteObject = new Note(initialNotes[0])
  21. await noteObject.save()
  22. noteObject = new Note(initialNotes[1])
  23. await noteObject.save()
  24. })
  25. test('notes are returned as json', async () => {
  26. await api
  27. .get('/api/notes')
  28. .expect(200)
  29. .expect('Content-Type', /application\/json/)
  30. }, 100000)
  31. -- snip --

修改最后2个测试

  1. test('all notes are returned', async () => {
  2. const response = await api.get('/api/notes')
  3. expect(response.body).toHaveLength(initialNotes.length)
  4. })
  5. test('a specific note is within the returned notes', async () => {
  6. const response = await api.get('/api/notes')
  7. const contents = response.body.map(r => r.content)
  8. expect(contents).toContain('Browser can execute only Javascript')
  9. })

Running tests one by one

运行指定文件的测试

  1. npm test -- tests/note_api.test.js

-t 选项可用于运行具有特定名称的测试

  1. npm test -- -t "a specific note is within the returned notes"

运行包含关键字的测试

  1. npm test -- -t 'notes'

async/await

async/await目的是使用异步调用函数来返回一个 promise,但使代码看起来像是同步调用。
普通的promise

  1. Note.find({})
  2. .then(notes => {
  3. return notes[0].remove()
  4. })
  5. .then(response => {
  6. console.log('the first note is removed')
  7. // more code here
  8. })

改用async/await

  1. const notes = await Note.find({})
  2. const response = await notes[0].remove()
  3. console.log('the first note is removed')

async/await避免了写回调函数
代码执行会在await处暂停等待,直到promise settle了才继续往下执行
需要注意的是: await返回的必须是一个promise, 且只能在async声明的函数中使用

async/await in the backend

  1. notesRouter.get('/', (request, response) => {
  2. Note.find({}).then(notes => {
  3. response.json(notes)
  4. })
  5. })

改为使用async/await

  1. notesRouter.get('/', async (request, response) => {
  2. const notes = await Note.find({})
  3. response.json(notes)
  4. })

Error handling and async/await

使用try…catch处理async/await错误

  1. notesRouter.post('/', async (request, response, next) => {
  2. const body = request.body
  3. const note = new Note({
  4. content: body.content,
  5. important: body.important || false,
  6. date: new Date()
  7. })
  8. try {
  9. const savedNote = await note.save()
  10. response.json(savedNote)
  11. } catch(exception) {
  12. next(exception)
  13. }
  14. })

Eliminating the try-catch

使用express-async-errors库可以消除try…catch
安装npm install express-async-errors
在app.js中引入库

  1. const config = require('./utils/config')
  2. const express = require('express')
  3. require('express-async-errors')
  4. -- snip --
  5. module.exports = app

现在可以直接将try…catch去掉,next(exception)也不再需要,库会自动处理错误

  1. notesRouter.delete('/:id', async (request, response) => {
  2. await Note.findByIdAndRemove(request.params.id)
  3. response.status(204).end()
  4. })

Optimizing the beforeEach function

使用Promise.all

  1. beforeEach(async () => {
  2. await Note.deleteMany({})
  3. const noteObjects = helper.initialNotes
  4. .map(note => new Note(note))
  5. const promiseArray = noteObjects.map(note => note.save())
  6. await Promise.all(promiseArray)
  7. })

await Promise.all会等待所有的promise都完成