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 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:
after
, after
, after
before each
, after each
hooks (very useful for cleaning the environment before the tests)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.
It is time to set up our book depository.
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.
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.
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:
config
module to access the configuration file in accordance with the environment variable NODE_ENV. From it we get the mongo db URI to connect to the database. This will allow us to keep the main database clean, and conduct tests on a separate base, hidden from users.let
. It makes the variable visible only within the closure block or globally if it is outside the block.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.
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:
Object.assign
, a new ES6 function that overwrites the general properties of book
and req.body
and leaves the req.body
intact.We finished this part and got the finished application!
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
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.
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!
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...
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:
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.
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:
morgan
logs to the console.chaiHttp
to chai
.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.
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.
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:
errors
.errors
field should have a pages
property missing in the request.pages
must have a kind
property equal to required
to show the reason why we received a negative response from the server.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:
callback
for the route / POST, then you saw that in case of an error, the server sends an error from mongoose
in response. Try it through POSTMAN and look at the answer.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:
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.
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.
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.
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