使用 Mocha 和 Chai 测试节点 RESTful API

介绍

我仍然记得最终能够在 node 中编写更大应用程序的后端部分的满足感,我相信你们中的许多人也这样做了。

然后?我们需要确保我们的应用程序按照我们期望的方式运行,强烈建议的方法之一是软件测试。每当向系统添加新功能时,软件测试都非常有用:已经设置了可以使用单个命令运行的测试环境有助于确定新功能是否引入了新错误。

过去,我们致力于使用 JSON Web Tokens 和 Passport进行Node API 身份验证

在本教程中,我们将使用 Node.js 编写一个简单的 RESTful API,并使用MochaChai编写针对它的测试。我们将在书店上测试 CRUD 操作。

像往常一样,您可以在整个教程中逐步构建应用程序,也可以直接在github上获取它

Mocha:测试环境

Mocha是 Node.js 的 javascript 框架,它允许异步测试。假设它提供了一个环境,我们可以在其中使用我们最喜欢的断言库来测试代码。

mocha-主页.

Mocha 有很多很棒的功能,网站显示了一个很长的列表,但这里是我最喜欢的:

  • 简单的异步支持,包括承诺。
  • 异步测试超时支持。
  • before, after, before each, after each hooks(对于清理每次测试的环境非常有用!)。
  • 使用任何你想要的断言库,我们教程中的 Chai。

Chai:断言库

因此,使用 Mocha 我们实际上拥有进行测试的环境,但是例如,我们如何测试 HTTP 调用?此外,在给定定义的输入的情况下,我们如何测试 GET 请求是否实际返回了我们期望的 JSON 文件?我们需要一个断言库,这就是为什么 mocha 是不够的。

所以这里是Chai,当前教程的断言库:

柴主页

Chai 在选择我们喜欢的界面的自由上大放异彩:“应该”、“期望”、“断言”它们都可用。我个人使用 should 但你可以自由地检查它的API并切换到其他两个。最后,Chai HTTP插件允许 Chai 库轻松使用符合我们需求的 HTTP 请求断言。

先决条件

  • Node.js:对 node.js 有基本的了解,推荐使用,因为我不会详细介绍构建 RESTful API。
  • POSTMAN 用于向 API 发出快速 HTTP 请求。
  • ES6 语法:我决定使用最新版本的 Node (6.*.*),它具有最高的 ES6 特性集成,以获得更好的代码可读性。如果你不熟悉 ES6,你可以看一下关于它的伟大的苏格兰文章(Pt.1Pt.2Pt.3),但不要担心每当我们遇到一些“异国情调的” 语法或声明。

是时候建立我们的书店了!

项目设置

目录结构

这是我们 API 的项目目录,您以前一定见过:

-- controllers 
---- models
------ book.js
---- routes
------ book.js
-- config
---- default.json
---- dev.json
---- test.json
-- test
---- book.js
package.json
server.json

请注意/config包含 3 个 JSON 文件文件夹:顾名思义,它们包含用于特定目的的特定配置。

在本教程中,我们将在两个数据库之间切换,一个用于开发,一个用于测试,因此文件包含 JSON 格式的 mongodb URI:

dev.json 和 default.json
{ "DBHost": "YOUR_DB_URI" }
测试文件
{ "DBHost": "YOUR_TEST_DB_URI" }

注意default.json是可选的,但是让我强调一下 config 目录中的文件是从它开始加载的。有关配置文件(配置目录、文件顺序、文件格式等)的更多信息,请查看此链接

最后,请注意/test/book.js,这就是我们要编写测试的地方!

包.json

创建package.json文件并粘贴以下代码:

{
  "name": "bookstore",
  "version": "1.0.0",
  "description": "A bookstore API",
  "main": "server.js",
  "author": "Sam",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.15.1",
    "config": "^1.20.1",
    "express": "^4.13.4",
    "mongoose": "^4.4.15",
    "morgan": "^1.7.0"
  },
  "devDependencies": {
    "chai": "^3.5.0",
    "chai-http": "^2.0.1",
    "mocha": "^2.4.5"
  },
  "scripts": {
    "start": "SET NODE_ENV=dev && node server.js",
    "test": "mocha --timeout 10000"
  }
}

同样,配置不应该让任何使用 node.js 编写更多服务器的人感到惊讶,与测试相关的包mochachaichai-http保存在 dev-dependencies(--save-dev来自命令行的标志)中,而 scripts 属性允许两个运行服务器的不同方式。

为了运行 mocha,我添加了该标志,--timeout 10000因为我从 mongolab 上托管的数据库中获取数据,因此默认的 2 秒可能不够。

恭喜!您已经完成了本教程的无聊部分,现在是编写服务器并对其进行测试的时候了。

服务器

主要的

让我们server.js在项目的根目录中创建文件并粘贴以下代码:


let express = require('express');
let app = express();
let mongoose = require('mongoose');
let morgan = require('morgan');
let bodyParser = require('body-parser');
let port = 8080;
let book = require('./app/routes/book');
let config = require('config'); //we load the db location from the JSON files
//db options
let options = { 
                server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } }, 
                replset: { socketOptions: { keepAlive: 1, connectTimeoutMS : 30000 } } 
              }; 

//db connection      
mongoose.connect(config.DBHost, options);
let db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));

//don't show the log when it is test
if(config.util.getEnv('NODE_ENV') !== 'test') {
    //use morgan to log at command line
    app.use(morgan('combined')); //'combined' outputs the Apache style LOGs
}

//parse application/json and look for raw text                                        
app.use(bodyParser.json());                                     
app.use(bodyParser.urlencoded({extended: true}));               
app.use(bodyParser.text());                                    
app.use(bodyParser.json({ type: 'application/json'}));  

app.get("/", (req, res) => res.json({message: "Welcome to our Bookstore!"}));

app.route("/book")
    .get(book.getBooks)
    .post(book.postBook);
app.route("/book/:id")
    .get(book.getBook)
    .delete(book.deleteBook)
    .put(book.updateBook);


app.listen(port);
console.log("Listening on port " + port);

module.exports = app; // for testing

以下是关键概念:

  • 我们要求模块配置访问名为NODE_ENV内容的配置文件,以获取数据库连接的 mongo db URI 参数。这有助于我们通过测试另一个对我们的应用程序未来用户隐藏的数据库来保持“真实”数据库的清洁。
  • 环境变量NODE_ENV是针对测试的测试,在命令行中禁用摩根日志,否则会干扰测试输出。
  • 最后一行代码导出服务器以进行测试。
  • 请注意使用 let 的变量定义,它使变量包含在最近的封闭块或全局(如果在任何块之外)。

剩下的代码行并不是什么新鲜事,我们只是通过要求所有必要的模块,定义与服务器通信的头选项,创建特定的根并最终让服务器侦听定义的端口。

模型和路线

是时候使用我们的书籍模型了!/app/model/调用中创建一个文件book.js并粘贴以下代码:

let mongoose = require('mongoose');
let Schema = mongoose.Schema;

//book schema definition
let BookSchema = new Schema(
  {
    title: { type: String, required: true },
    author: { type: String, required: true },
    year: { type: Number, required: true },
    pages: { type: Number, required: true, min: 1 },
    createdAt: { type: Date, default: Date.now },    
  }, 
  { 
    versionKey: false
  }
);

// Sets the createdAt parameter equal to the current time
BookSchema.pre('save', next => {
  now = new Date();
  if(!this.createdAt) {
    this.createdAt = now;
  }
  next();
});

//Exports the BookSchema for use elsewhere.
module.exports = mongoose.model('book', BookSchema);

我们的图书模式在数据库中有标题、作者、页数、出版年份和创建日期。我将 versionKey 设置为 false,因为它对本教程的目的没有用。

注意.pre()函数中的奇特回调语法是一个箭头函数,一个具有较短语法的函数,根据MDN 上的定义 ,“词法绑定 this 值(不绑定它自己的 this、arguments、super 或 new 。目标)。箭头函数总是匿名的”

嗯,几乎所有我们需要了解的模型,所以让我们转向路线​​。

/app/routes/创建一个名为 book.js 的文件并粘贴以下代码:

let mongoose = require('mongoose');
let Book = require('../models/book');

/*
 * GET /book route to retrieve all the books.
 */
function getBooks(req, res) {
    //Query the DB and if no errors, send all the books
    let query = Book.find({});
    query.exec((err, books) => {
        if(err) res.send(err);
        //If no errors, send them back to the client
        res.json(books);
    });
}

/*
 * POST /book to save a new book.
 */
function postBook(req, res) {
    //Creates a new book
    var newBook = new Book(req.body);
    //Save it into the DB.
    newBook.save((err,book) => {
        if(err) {
            res.send(err);
        }
        else { //If no errors, send it back to the client
            res.json({message: "Book successfully added!", book });
        }
    });
}

/*
 * GET /book/:id route to retrieve a book given its id.
 */
function getBook(req, res) {
    Book.findById(req.params.id, (err, book) => {
        if(err) res.send(err);
        //If no errors, send it back to the client
        res.json(book);
    });        
}

/*
 * DELETE /book/:id to delete a book given its id.
 */
function deleteBook(req, res) {
    Book.remove({_id : req.params.id}, (err, result) => {
        res.json({ message: "Book successfully deleted!", result });
    });
}

/*
 * PUT /book/:id to updatea a book given its id
 */
function updateBook(req, res) {
    Book.findById({_id: req.params.id}, (err, book) => {
        if(err) res.send(err);
        Object.assign(book, req.body).save((err, book) => {
            if(err) res.send(err);
            res.json({ message: 'Book updated!', book });
        });    
    });
}

//export all the functions
module.exports = { getBooks, postBook, getBook, deleteBook, updateBook };

这里的关键概念:

  • 路由只不过是标准路由,GET、POST、DELETE、PUT 来对我们的数据执行 CRUD 操作。
  • updatedBook()我们使用Object.assign的函数中,ES6 中引入了一个新函数,在这种情况下,它覆盖了 book 的公共属性,req.body同时保持其他属性不变。
  • 最后,我们使用更快的语法导出对象,该语法将键和值配对以避免无用的重复。

我们完成了这一部分,实际上我们有一个可以运行的应用程序!

天真的测试

现在让我们运行应用程序并打开 POSTMAN 向服务器发送 HTTP 请求并检查一切是否按预期工作。

在命令行运行

npm start

获取/预订

在 POSTMAN 中运行 GET 请求,假设数据库包含书籍,结果如下:

服务器正确返回了我数据库中的图书列表。

发布/书

让我们添加一本书并 POST 到服务器:

看来这本书被完美地添加了。服务器返回了这本书,并显示了一条消息,确认它已添加到我们的书店中。这是真的吗?让我们发送另一个 GET 请求,结果如下:

太棒了!

PUT /book/:id

让我们通过更改页面来更新一本书并检查结果:

伟大的!PUT 似乎也在工作,所以让我们发送另一个 GET 请求来检查所有列表:

一切进展顺利……

获取 /book/:id

现在让我们通过在 GET 请求中发送 id 来获取一本书,然后将其删除:

当它返回正确的书时,让我们现在尝试删除它:

删除 /book/:id

这是对服务器的 DELETE 请求的结果:

即使最后一个请求也能顺利运行,我们不需要再次检查另一个 GET 请求,因为我们正在向客户端发送来自 mongo(结果属性)的一些信息,这些信息表明这本书实际上已被删除。

通过对 POSTMAN 进行一些测试,该应用程序碰巧按预期运行,对吗?那么,你会把它拍给你的客户吗?

让我来回答你:NO!!

我们的测试是我所说的幼稚测试,因为我们只是尝试了一些操作,而没有测试可能发生的奇怪情况:没有一些预期数据的 post 请求,带有错误 id 作为参数的 DELETE,甚至没有 id 等等。

这显然是一个简单的应用程序,如果我们足够幸运的话,我们编码它而没有引入任何类型的错误,但是真实世界的应用程序呢?此外,我们还花时间与 POSTMAN 一起运行一些测试 HTTP 请求,那么如果有一天我们不得不更改其中之一的代码会发生什么?用 POSTMAN 再次测试它们?您是否开始意识到这不是一种敏捷方法?

这只不过是您可能遇到的少数情况,并且您在作为开发人员的旅程中已经遇到过,幸运的是,我们有工具来创建始终可用的测试,并且可以通过单个命令行启动。

让我们做一些更好的事情来测试我们的应用程序!

更好的测试

首先,让我们在/test调用中创建一个文件book.js并粘贴以下代码:

//During the test the env variable is set to test
process.env.NODE_ENV = 'test';

let mongoose = require("mongoose");
let Book = require('../app/models/book');

//Require the dev-dependencies
let chai = require('chai');
let chaiHttp = require('chai-http');
let server = require('../server');
let should = chai.should();


chai.use(chaiHttp);
//Our parent block
describe('Books', () => {
    beforeEach((done) => { //Before each test we empty the database
        Book.remove({}, (err) => { 
           done();           
        });        
    });
/*
  * Test the /GET route
  */
  describe('/GET book', () => {
      it('it should GET all the books', (done) => {
        chai.request(server)
            .get('/book')
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('array');
                  res.body.length.should.be.eql(0);
              done();
            });
      });
  });

});

哇,有很多新东西,让我们深入研究一下:

  1. 您一定已经注意到我们将NODE_ENV变量设置为 test,通过这样做我们更改了要加载的配置文件,以便服务器连接到测试数据库并避免 cmd 中的摩根日志。
  2. 我们需要 dev-dependencies 模块和服务器本身(你还记得我们是通过 导出的module.exports吗?)。

  3. 我们should通过运行chai.should()定义对 HTTP 请求结果的测试样式,然后我们告诉 chai 使用 chai HTTP。

所以它从“描述”代码块开始,以便更好地组织你的断言,这个组织将反映在命令行的输出中,我们将在后面看到。

beforeEach是将在同一级别上的每个描述块之前运行的代码块。我们为什么这样做?好吧,每当运行测试时,我们将从数据库中删除任何书籍,以从一个空书店开始。

测试 /GET 路由

这是第一个测试,chai 将向服务器执行 GET 请求,并且 res 变量上的断言将满足或拒绝它应该获取所有书籍it的第一个参数准确地说,给定空书店,请求的结果应该是:

  1. 状态 200。
  2. 结果应该是一个数组。
  3. 由于书店是空的,我们假设长度等于 0。

请注意,should断言的语法非常直观,因为它类似于自然语言语句。

现在,在命令行中运行:

“`javascript npm 测试”`

这是输出:

测试通过,输出反映了我们使用describe.

测试 /POST 路由

现在让我们检查我们强大的 API,假设我们正在尝试添加一本书,其中缺少页面字段传递给服务器:服务器不应以正确的错误消息响应。

将以下代码复制并粘贴到测试文件中:

process.env.NODE_ENV = 'test';

let mongoose = require("mongoose");
let Book = require('../app/models/book');

let chai = require('chai');
let chaiHttp = require('chai-http');
let server = require('../server');
let should = chai.should();


chai.use(chaiHttp);

describe('Books', () => {
    beforeEach((done) => {
        Book.remove({}, (err) => { 
           done();           
        });        
    });
  describe('/GET book', () => {
      it('it should GET all the books', (done) => {
        chai.request(server)
            .get('/book')
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('array');
                  res.body.length.should.be.eql(0);
              done();
            });
      });
  });
  /*
  * Test the /POST route
  */
  describe('/POST book', () => {
      it('it should not POST a book without pages field', (done) => {
          let book = {
              title: "The Lord of the Rings",
              author: "J.R.R. Tolkien",
              year: 1954
          }
        chai.request(server)
            .post('/book')
            .send(book)
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('object');
                  res.body.should.have.property('errors');
                  res.body.errors.should.have.property('pages');
                  res.body.errors.pages.should.have.property('kind').eql('required');
              done();
            });
      });

  });
});

这里我们在不完整的 /POST 请求上添加了测试,让我们分析断言:

  1. 状态应该是 200。
  2. 响应主体应该是一个对象。
  3. 身体属性之一应该是errors.
  4. Errors 应该将缺少的字段页面作为属性。
  5. 最后pages应该有kind等于required的属性,以突出我们从服务器得到否定答案的原因。

请注意,我们通过.send()函数将书与 POST 请求一起发送

让我们再次运行相同的命令,输出如下:

哦,是的,我们的测试测试是正确的!

在编写新测试之前,让我明确两件事:

  1. 首先,为什么服务器响应以这种方式构建?如果您阅读 /POST 路由的回调函数,您会注意到如果缺少必填字段,服务器会从 mongoose 发回错误消息。尝试使用 POSTMAN 并检查响应。
  2. 如果缺少字段,我们仍然返回 200 状态,这是为了简单起见,因为我们只是在学习测试我们的路线。但是我建议改为返回206 Partial Content的状态

这次让我们发送一本书,其中包含所有必填字段。将以下代码复制并粘贴到测试文件中:

process.env.NODE_ENV = 'test';

let mongoose = require("mongoose");
let Book = require('../app/models/book');

let chai = require('chai');
let chaiHttp = require('chai-http');
let server = require('../server');
let should = chai.should();


chai.use(chaiHttp);

describe('Books', () => {
    beforeEach((done) => {
        Book.remove({}, (err) => { 
           done();           
        });        
    });
  describe('/GET book', () => {
      it('it should GET all the books', (done) => {
        chai.request(server)
            .get('/book')
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('array');
                  res.body.length.should.be.eql(0);
              done();
            });
      });
  });
  /*
  * Test the /POST route
  */
  describe('/POST book', () => {
      it('it should not POST a book without pages field', (done) => {
          let book = {
              title: "The Lord of the Rings",
              author: "J.R.R. Tolkien",
              year: 1954
          }
        chai.request(server)
            .post('/book')
            .send(book)
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('object');
                  res.body.should.have.property('errors');
                  res.body.errors.should.have.property('pages');
                  res.body.errors.pages.should.have.property('kind').eql('required');
              done();
            });
      });
      it('it should POST a book ', (done) => {
          let book = {
              title: "The Lord of the Rings",
              author: "J.R.R. Tolkien",
              year: 1954,
              pages: 1170
          }
        chai.request(server)
            .post('/book')
            .send(book)
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('object');
                  res.body.should.have.property('message').eql('Book successfully added!');
                  res.body.book.should.have.property('title');
                  res.body.book.should.have.property('author');
                  res.body.book.should.have.property('pages');
                  res.body.book.should.have.property('year');
              done();
            });
      });
  });
});

这次我们期望返回一个带有消息的对象,说明我们成功地添加了这本书和这本书本身(还记得 POSTMAN 吗?)。您现在应该对我所做的断言非常熟悉,因此无需详细说明。相反,再次运行该命令,输出如下:

光滑~

测试 /GET/:id 路由

现在让我们创建一本书,将其保存到数据库中并使用 id 向服务器发送 GET 请求。将以下代码复制并粘贴到测试文件中:

process.env.NODE_ENV = 'test';

let mongoose = require("mongoose");
let Book = require('../app/models/book');

let chai = require('chai');
let chaiHttp = require('chai-http');
let server = require('../server');
let should = chai.should();


chai.use(chaiHttp);

describe('Books', () => {
    beforeEach((done) => {
        Book.remove({}, (err) => { 
           done();           
        });        
    });
  describe('/GET book', () => {
      it('it should GET all the books', (done) => {
            chai.request(server)
            .get('/book')
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('array');
                  res.body.length.should.be.eql(0);
              done();
            });
      });
  });
  describe('/POST book', () => {
      it('it should not POST a book without pages field', (done) => {
          let book = {
              title: "The Lord of the Rings",
              author: "J.R.R. Tolkien",
              year: 1954
          }
            chai.request(server)
            .post('/book')
            .send(book)
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('object');
                  res.body.should.have.property('errors');
                  res.body.errors.should.have.property('pages');
                  res.body.errors.pages.should.have.property('kind').eql('required');
              done();
            });
      });
      it('it should POST a book ', (done) => {
          let book = {
              title: "The Lord of the Rings",
              author: "J.R.R. Tolkien",
              year: 1954,
              pages: 1170
          }
            chai.request(server)
            .post('/book')
            .send(book)
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('object');
                  res.body.should.have.property('message').eql('Book successfully added!');
                  res.body.book.should.have.property('title');
                  res.body.book.should.have.property('author');
                  res.body.book.should.have.property('pages');
                  res.body.book.should.have.property('year');
              done();
            });
      });
  });
 /*
  * Test the /GET/:id route
  */
  describe('/GET/:id book', () => {
      it('it should GET a book by the given id', (done) => {
          let book = new Book({ title: "The Lord of the Rings", author: "J.R.R. Tolkien", year: 1954, pages: 1170 });
          book.save((err, book) => {
              chai.request(server)
            .get('/book/' + book.id)
            .send(book)
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('object');
                  res.body.should.have.property('title');
                  res.body.should.have.property('author');
                  res.body.should.have.property('pages');
                  res.body.should.have.property('year');
                  res.body.should.have.property('_id').eql(book.id);
              done();
            });
          });

      });
  });
});

通过断言,我们确保服务器返回所有字段和正确的书籍,一起测试两个ID这是输出:

您是否注意到通过测试独立块内的单个路由,我们提供了非常清晰的输出?还有,效率不是很高吗?我们编写了几个可以用单个命令行重复的测试,一劳永逸。

测试 /PUT/:id 路由

是时候测试我们其中一本书的更新了,我们首先保存这本书,然后更新它的出版年份。因此,复制并粘贴以下代码:

process.env.NODE_ENV = 'test';

let mongoose = require("mongoose");
let Book = require('../app/models/book');

let chai = require('chai');
let chaiHttp = require('chai-http');
let server = require('../server');
let should = chai.should();


chai.use(chaiHttp);

describe('Books', () => {
    beforeEach((done) => {
        Book.remove({}, (err) => { 
           done();           
        });        
    });
  describe('/GET book', () => {
      it('it should GET all the books', (done) => {
            chai.request(server)
            .get('/book')
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('array');
                  res.body.length.should.be.eql(0);
              done();
            });
      });
  });
  describe('/POST book', () => {
      it('it should not POST a book without pages field', (done) => {
          let book = {
              title: "The Lord of the Rings",
              author: "J.R.R. Tolkien",
              year: 1954
          }
            chai.request(server)
            .post('/book')
            .send(book)
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('object');
                  res.body.should.have.property('errors');
                  res.body.errors.should.have.property('pages');
                  res.body.errors.pages.should.have.property('kind').eql('required');
              done();
            });
      });
      it('it should POST a book ', (done) => {
          let book = {
              title: "The Lord of the Rings",
              author: "J.R.R. Tolkien",
              year: 1954,
              pages: 1170
          }
            chai.request(server)
            .post('/book')
            .send(book)
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('object');
                  res.body.should.have.property('message').eql('Book successfully added!');
                  res.body.book.should.have.property('title');
                  res.body.book.should.have.property('author');
                  res.body.book.should.have.property('pages');
                  res.body.book.should.have.property('year');
              done();
            });
      });
  });
  describe('/GET/:id book', () => {
      it('it should GET a book by the given id', (done) => {
          let book = new Book({ title: "The Lord of the Rings", author: "J.R.R. Tolkien", year: 1954, pages: 1170 });
          book.save((err, book) => {
              chai.request(server)
            .get('/book/' + book.id)
            .send(book)
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('object');
                  res.body.should.have.property('title');
                  res.body.should.have.property('author');
                  res.body.should.have.property('pages');
                  res.body.should.have.property('year');
                  res.body.should.have.property('_id').eql(book.id);
              done();
            });
          });

      });
  });
 /*
  * Test the /PUT/:id route
  */
  describe('/PUT/:id book', () => {
      it('it should UPDATE a book given the id', (done) => {
          let book = new Book({title: "The Chronicles of Narnia", author: "C.S. Lewis", year: 1948, pages: 778})
          book.save((err, book) => {
                chai.request(server)
                .put('/book/' + book.id)
                .send({title: "The Chronicles of Narnia", author: "C.S. Lewis", year: 1950, pages: 778})
                .end((err, res) => {
                      res.should.have.status(200);
                      res.body.should.be.a('object');
                      res.body.should.have.property('message').eql('Book updated!');
                      res.body.book.should.have.property('year').eql(1950);
                  done();
                });
          });
      });
  });
});

我们要确保消息是正确的 Book 更新!一,该year字段实际上已更新。这是输出:

好的,我们已经接近尾声了,我们还需要测试 DELETE 路线。

测试 /DELETE/:id 路由

该模式与之前的测试类似,我们首先存储一本书,将其删除并针对响应进行测试。复制并粘贴以下代码:

process.env.NODE_ENV = 'test';

let mongoose = require("mongoose");
let Book = require('../app/models/book');

let chai = require('chai');
let chaiHttp = require('chai-http');
let server = require('../server');
let should = chai.should();


chai.use(chaiHttp);

describe('Books', () => {
    beforeEach((done) => {
        Book.remove({}, (err) => { 
           done();           
        });        
    });
  describe('/GET book', () => {
      it('it should GET all the books', (done) => {
            chai.request(server)
            .get('/book')
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('array');
                  res.body.length.should.be.eql(0);
              done();
            });
      });
  });
  describe('/POST book', () => {
      it('it should not POST a book without pages field', (done) => {
          let book = {
              title: "The Lord of the Rings",
              author: "J.R.R. Tolkien",
              year: 1954
          }
            chai.request(server)
            .post('/book')
            .send(book)
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('object');
                  res.body.should.have.property('errors');
                  res.body.errors.should.have.property('pages');
                  res.body.errors.pages.should.have.property('kind').eql('required');
              done();
            });
      });
      it('it should POST a book ', (done) => {
          let book = {
              title: "The Lord of the Rings",
              author: "J.R.R. Tolkien",
              year: 1954,
              pages: 1170
          }
            chai.request(server)
            .post('/book')
            .send(book)
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('object');
                  res.body.should.have.property('message').eql('Book successfully added!');
                  res.body.book.should.have.property('title');
                  res.body.book.should.have.property('author');
                  res.body.book.should.have.property('pages');
                  res.body.book.should.have.property('year');
              done();
            });
      });
  });
  describe('/GET/:id book', () => {
      it('it should GET a book by the given id', (done) => {
          let book = new Book({ title: "The Lord of the Rings", author: "J.R.R. Tolkien", year: 1954, pages: 1170 });
          book.save((err, book) => {
              chai.request(server)
            .get('/book/' + book.id)
            .send(book)
            .end((err, res) => {
                  res.should.have.status(200);
                  res.body.should.be.a('object');
                  res.body.should.have.property('title');
                  res.body.should.have.property('author');
                  res.body.should.have.property('pages');
                  res.body.should.have.property('year');
                  res.body.should.have.property('_id').eql(book.id);
              done();
            });
          });

      });
  });
  describe('/PUT/:id book', () => {
      it('it should UPDATE a book given the id', (done) => {
          let book = new Book({title: "The Chronicles of Narnia", author: "C.S. Lewis", year: 1948, pages: 778})
          book.save((err, book) => {
                chai.request(server)
                .put('/book/' + book.id)
                .send({title: "The Chronicles of Narnia", author: "C.S. Lewis", year: 1950, pages: 778})
                .end((err, res) => {
                      res.should.have.status(200);
                      res.body.should.be.a('object');
                      res.body.should.have.property('message').eql('Book updated!');
                      res.body.book.should.have.property('year').eql(1950);
                  done();
                });
          });
      });
  });
 /*
  * Test the /DELETE/:id route
  */
  describe('/DELETE/:id book', () => {
      it('it should DELETE a book given the id', (done) => {
          let book = new Book({title: "The Chronicles of Narnia", author: "C.S. Lewis", year: 1948, pages: 778})
          book.save((err, book) => {
                chai.request(server)
                .delete('/book/' + book.id)
                .end((err, res) => {
                      res.should.have.status(200);
                      res.body.should.be.a('object');
                      res.body.should.have.property('message').eql('Book successfully deleted!');
                      res.body.result.should.have.property('ok').eql(1);
                      res.body.result.should.have.property('n').eql(1);
                  done();
                });
          });
      });
  });
});

服务器再次从 mongoose 返回我们断言的消息和属性,因此让我们检查输出:

太好了,我们的测试都是正面的,我们有一个很好的基础来继续使用更复杂的断言测试我们的路线。

恭喜您完成教程!

结论

在本教程中,我们面临的问题是测试我们的路线以向我们的用户提供稳定的体验。

我们经历了创建 RESTful API 的所有步骤,使用 POSTMAN 进行了简单的测试,然后提出了一种更好的测试方法,这实际上是本教程的主要主题。

总是花一些时间进行测试以确保服务器尽可能可靠是一个好习惯,但不幸的是它经常被低估。

在本教程中,我们还将讨论代码测试的一些好处,这将为更高级的主题打开大门,例如测试驱动开发 (TDD)。

觉得文章有用?

点个广告表达一下你的爱意吧 !😁