📜 ⬆️ ⬇️

Testing RESTful API on NodeJS with Mocha and Chai


Translation guide Samuele Zaza . The text of the original article can be found here .


I still remember the delight of the possibility of finally writing the backend of a large project on a node, and I am sure that many people share my feelings.


What's next? We must be sure that our application behaves as we expect. One of the most common ways to achieve this is with tests. Testing is an incredibly useful thing when we add a new feature to the application: the presence of an already installed and configured test environment, which can be started by one team, helps to understand where a new fig will generate new bugs.
We have previously discussed the development of the RESTful Node API and Node API authentication . In this tutorial, we will write a simple RESTful API and use Mocha and Chai to test it. We will test CRUD for the book deposit app.


As always, you can do everything in steps, reading the manual, or download the source code on github .


Mocha: Test Environment


Mocha is a javascript framework for Node.js, which allows asynchronous testing. Let's just say: it creates an environment in which we can use our favorite assert libraries.



Mocha comes with a huge amount of features. The site has a huge list of them. Most of all I like the following:



Chai: assertion library


So, with Mocha, we have an environment to perform our tests, but how will we test HTTP requests, for example? Moreover, how to verify that the GET request returned the expected JSON to the response, depending on the parameters passed? We need an assertion library, because mocha is obviously not enough.


For this tutorial, I chose Chai:



Chai gives us a set of interface choices: "should", "expect", "assert". I personally use should, but you can choose any. In addition, Chai has a plugin Chai HTTP, which allows you to easily test HTTP requests.


PREREQUISITES



It is time to set up our book depository.


Project Setup


Folder structure


The project structure will be as follows:


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

Please note that the /config folder contains 3 JSON files: as the name implies, they contain settings for different environments.


In this tutorial, we will switch between two databases — one for development, the other for testing. Thus, the files will contain mongodb URI in JSON format:


dev.json and default.json :


 { "DBHost": "YOUR_DB_URI" } 

test.json :


 { "DBHost": "YOUR_TEST_DB_URI" } 

More about the configuration files (config folder, file order, file format) can be read [here] ( https://github.com/lorenwest/node-config/wiki/Configuration-Files ).


Pay attention to the file /test/book.js , which will be all our tests.


package.json


Create a package.json file and paste in the following:


 { "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" } } 

Again, nothing new for someone who has written at least one server on node.js. The mocha , chai , chai-http packages required for testing are installed in the dev-dependencies block (the --save-dev flag from the command line).
The scripts block contains two ways to start the server.


For mocha, I added the --timeout 10000 flag, because I am picking up data from the base located on mongolab and being released two seconds by default may not be enough.


Hooray! We finished the boring part of the manual and it’s time to write a server and test it.


Server


Let's create a server.js file and server.js following code:


 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'); //      //  let options = { server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } }, replset: { socketOptions: { keepAlive: 1, connectTimeoutMS : 30000 } } }; //   mongoose.connect(config.DBHost, options); let db = mongoose.connection; db.on('error', console.error.bind(console, 'connection error:')); //      if(config.util.getEnv('NODE_ENV') !== 'test') { //morgan      app.use(morgan('combined')); //'combined'     apache } // application/json 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; //   

Highlights:



The rest is nothing new: we simply connect the necessary modules, define the settings for interacting with the server, create entry points and start the server on a specific port.


Models and Routing


It is time to describe the model of the book. Create a book.js file in the /app/model/ folder with the following contents:


 let mongoose = require('mongoose'); let Schema = mongoose.Schema; //   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 } ); //   createdAt    BookSchema.pre('save', next => { now = new Date(); if(!this.createdAt) { this.createdAt = now; } next(); }); //    . module.exports = mongoose.model('book', BookSchema); 

Our book has a title, author, number of pages, year of publication and date of creation in the database. I set the versionKey option to false , since it is not needed in this guide.


An unusual callback in .pre () is a shooter function, a function with a shorter syntax. According to the definition of MDN : "binds to the current value of this (does not have its own this , arguments , super , or new.target ). The arrow functions are always anonymous."


Great, now we know everything we need about the model and go to the routes.


In the folder /app/routes/ create a book.js file book.js following contents:


 let mongoose = require('mongoose'); let Book = require('../models/book'); /* * GET /book      . */ function getBooks(req, res) { //    ,   ,     let query = Book.find({}); query.exec((err, books) => { if(err) res.send(err); //  ,   res.json(books); }); } /* * POST /book    . */ function postBook(req, res) { //   var newBook = new Book(req.body); //  . newBook.save((err,book) => { if(err) { res.send(err); } else { //  ,    res.json({message: "Book successfully added!", book }); } }); } /* * GET /book/:id      ID. */ function getBook(req, res) { Book.findById(req.params.id, (err, book) => { if(err) res.send(err); //  ,    res.json(book); }); } /* * DELETE /book/:id      ID. */ function deleteBook(req, res) { Book.remove({_id : req.params.id}, (err, result) => { res.json({ message: "Book successfully deleted!", result }); }); } /* * PUT /book/:id      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 }); }); }); } //   module.exports = { getBooks, postBook, getBook, deleteBook, updateBook }; 

Highlights:



We finished this part and got the finished application!


Naive testing


Let's run our application, open POSTMAN to send HTTP requests to the server and check that everything works as expected.


On the command line, we will


 npm start 

GET / BOOK


In POSTMAN, we will execute a GET request and, if we assume that there are books in the database, we will get the answer:
:


Server without errors returned books from the database.


POST / BOOK


Let's add a new book:



It looks like the book has been added. The server returned the book and a message confirming that it was added. Is it so? Perform another GET request and see the result:



Works!


PUT / BOOK /: ID


Let's change the number of pages in the book and look at the result:



Fine! PUT also works, so you can perform another GET request to check



Everything is working...


GET / BOOK /: ID


Now we will get one book by ID in the GET request and then delete it:



Received the correct answer and now delete this book:


DELETE / BOOK /: ID


Let's look at the removal result:



Even the last request works as planned and we don’t even need to make another GET request for verification, since we sent the client a response from mongo (result property), which indicates that the book was really deleted.


When running a test through POSTMAN, the application behaves as expected, right? So you can use it on the client?


Let me answer you: NO !!


I call our actions naive testing, because we performed only a few operations without taking into account controversial cases: POST request without expected data, DELETE with invalid id or no id at all.


Obviously this is a simple application and, if we are lucky, we have not made any mistakes, but what about real applications? Moreover, we spent time launching some test HTTP requests in POSTMAN. And what happens if one day we decide to change the code of one of them? Again, check everything in POSTMAN?


These are just a few situations that you may encounter or have already encountered as a developer. Fortunately, we have the tools to create tests that are always available; they can be run by a single command from the console.


Let's do something better to test our application.


Good testing


First, let's create the books.js file in the /test folder:


 //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'); // dev-dependencies 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(); }); }); /* *   /GET */ 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(); }); }); }); }); 

How many new pieces! Let's figure it out:



It all starts with the describe block, which is used to improve the structuring of our statements. This will affect the output, as we will see later.


beforeEach is the block that will be executed for each block described in this describe block. What are we doing this for? We delete all books from the database so that the base is empty at the beginning of each test.


Testing / GET


So, we have the first test. Chai executes the GET request and checks that the variable res satisfies the first parameter (statement) of the it "it should GET all the books" block. Namely, for this empty book depository, the answer should be as follows:



Note that the should syntax is intuitive and very similar to spoken language.


Terer command line vypon:


 npm test 

and get:


The test passed and the output reflects the structure we described using the describe blocks.


Testing / POST


Now let's check how good our API is. Suppose we are trying to add a book without the `pages: field: the server should not return an appropriate error.


Add this code to the end of the describe('Books') block:


 describe('/POST book', () => { it('it should not POST a book without pages field', (done) => { let book = { title: "The Lord of the Rings", author: "JRR 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(); }); }); }); 

Here we added a test for incomplete / POST request. Let's look at the checks:



Note that we sent the book data using the .send () method.


Let's execute the command again and look at the output:



The test works !!


Before writing the following test, let's clarify a couple of things:



However, I would suggest giving back the status of 206 Partial Content instead


Let's send the correct request. Paste the following code at the end of the describe(''/POST book'') block describe(''/POST book'') :


 it('it should POST a book ', (done) => { let book = { title: "The Lord of the Rings", author: "JRR 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(); }); }); 

This time we expect an object telling us that the book has been added successfully and the book itself. You should already be familiar with the checks, so there is no need to go into details.


Run the command again and get:



Testing / GET /: ID


Now we will create a book, save it in the database and use the id to execute the GET request. Add the following block:


 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: "JRR 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(); }); }); }); }); 

Through asserts we made sure that the server returned all the fields and the required book (the id in the response from the north coincides with the requested one):



Have you noticed that in testing individual routes inside independent blocks we got a very clean conclusion? Besides, this is effective: we wrote several tests that can be repeated using one command.


Testing / PUT /: ID


It is time to check the editing of one of our books. First, we will save the book in the database, and then we will issue a query to change the year of its publication.


 describe('/PUT/:id book', () => { it('it should UPDATE a book given the id', (done) => { let book = new Book({title: "The Chronicles of Narnia", author: "CS Lewis", year: 1948, pages: 778}) book.save((err, book) => { chai.request(server) .put('/book/' + book.id) .send({title: "The Chronicles of Narnia", author: "CS 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(); }); }); }); }); 

We want to make sure the message field is Book updated! and the year field has really changed.



We are almost done.


Test / DELETE /: ID.


The template is very similar to the previous test: first we create a book, then we delete it using the request and check the answer:


 describe('/DELETE/:id book', () => { it('it should DELETE a book given the id', (done) => { let book = new Book({title: "The Chronicles of Narnia", author: "CS 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(); }); }); }); }); 

Again the server will return to us the answer from mongoose , which we check. The console will have the following:



Amazing! Our tests pass and we have an excellent base for testing our API with the help of more sophisticated checks.


Conclusion


In this lesson, we faced the challenge of testing our routes in order to provide our users with a stable API.


We went through all the steps of creating a RESTful API, doing naive tests with POSTMAN, and then offered the best way to test our main goal.


Writing tests is a good habit to ensure server stability. Unfortunately, this is often underestimated.


Bonus: Mockgoose


There will always be someone who says that two bases are not the best solution, but there is no other. And what to do? The alternative is: Mockgoose.


In essence, Mockgoose creates a wrapper for Mongoose, which intercepts calls to the database and instead uses in-memory storage. It also integrates easily with mocha.


Note: Mockgoose requires mongodb to be installed on the machine where the tests are run.


')

Source: https://habr.com/ru/post/308352/


All Articles