⬆️ ⬇️

Testing the nodejs application

Last time I wrote about creating a nodejs application using expressjs as a framework and jade as a template engine. This time I want to dwell on testing the server side.



For tests we will use:

- Mocha - a framework that allows you to write tests and run easily. Generates reports in various ways, as well as being able to create documentation from tests.

- Should - library for tests in the style of "approval" (I did not find the correct name)

- SuperTest - library for testing HTTP servers on nodejs

- jscoverage - to assess code coverage tests





')

I did not write everything from scratch, but decided to wrap the application from the previous article in the tests, completely copying it into the app2 folder.



First of all, add the modules we need to the package.json file. We will need: mocha, should, supertest.

package.json
{ "name": "app2", "version": "0.0.0", "author": "Evgeny Reznichenko <kusakyky@gmaill.com>", "dependencies": { "express": "3", "jade": "*", "should": "*", "mocha": "*", "supertest": "*" } } 


Run the npm i command to install all required modules.

And install jscoverage (under Ubuntu sudo apt-get install jscoverage ).



Next, create a lib directory in the root of the project and copy our app.js there, it is necessary to cover all the scripts with tests for coverage.

Let's edit the app.js file so that it exports our server to the outside, and in the root of the project we will create an index.js file that will connect the server and hang it on the socket. And do not forget to correct the path to the views and public directories.

It should turn out like this:

index.js
 var app = require('./lib/app.js'); app.listen(3000); 




lib / app.js
 var express = require('express'), jade = require('jade'), fs = require('fs'), app = express(), viewOptions = { compileDebug: false, self: true }; //data var db = { users: [ { id: 0, name: 'Jo', age: 20, sex: 'm' }, { id: 1, name: 'Bo', age: 19, sex: 'm' }, { id: 2, name: 'Le', age: 18, sex: 'w' }, { id: 10, name: 'NotFound', age: 18, sex: 'w' } ], titles: { '/users': ' ', '/users/profile': ' ' } }; //utils function merge(a, b) { var key; if (a && b) { for (key in b) { a[key] = b[key]; } } return a; } //App settings app.set('views', __dirname + '/../views'); app.set('view engine', 'jade'); app.set('title', ' '); app.locals.compileDebug = viewOptions.compileDebug; app.locals.self = viewOptions.self; app.use(express.static(__dirname + '/../public')); app.use(app.router); app.use(function (req, res, next) { next('not found'); }); //error app.use(function (err, req, res, next) { if (/not found/i.test(err)) { res.locals.title = '  :('; res.render('/errors/notfound'); } else { res.locals.title = ''; res.render('/errors/error'); } }); app.use(express.errorHandler()); //routes //  app.all('*', function replaceRender(req, res, next) { var render = res.render, view = req.path.length > 1 ? req.path.substr(1).split('/'): []; res.render = function(v, o) { var data, title = res.locals.title; res.render = render; res.locals.title = app.get('title') + (title ? ' - ' + title: ''); //         //  if ('string' === typeof v) { if (/^\/.+/.test(v)) { view = v.substr(1).split('/'); } else { view = view.concat(v.split('/')); } data = o; } else { data = v; } // res.locals      //     (res.locals.title) data = merge(data || {}, res.locals); if (req.xhr) { //     json res.json({ data: data, view: view.join('.') }); } else { //   ,    // (   history api) data.state = JSON.stringify({ data: data, view: view.join('.') }); //    .       . view[view.length - 1] = '_' + view[view.length - 1]; //   res.render(view.join('/'), data); } }; next(); }); //   app.all('*', function loadPageTitle(req, res, next) { res.locals.title = db.titles[req.path]; next(); }); app.get('/', function(req, res){ res.render('index'); }); app.get('/users', function(req, res){ var data = { users: db.users }; res.render('index', data); }); app.get('/users/profile', function(req, res, next){ var user = db.users[req.query.id], data = { user: user }; if (user) { res.render(data); } else { next('Not found'); } }); // function loadTemplate(viewpath) { var fpath = app.get('views') + viewpath, str = fs.readFileSync(fpath, 'utf8'); viewOptions.filename = fpath; viewOptions.client = true; return jade.compile(str, viewOptions).toString(); } app.get('/templates', function(req, res) { var str = 'var views = { ' + '"index": (function(){ return ' + loadTemplate('/index.jade') + ' }()),' + '"users.index": (function(){ return ' + loadTemplate('/users/index.jade') + ' }()),' + '"users.profile": (function(){ return ' + loadTemplate('/users/profile.jade') + ' }()),' + '"errors.error": (function(){ return ' + loadTemplate('/errors/error.jade') + ' }()),' + '"errors.notfound": (function(){ return ' + loadTemplate('/errors/notfound.jade') + ' }())' + '};' res.set({ 'Content-type': 'text/javascript' }).send(str); }); module.exports = app; 






Let's take a look at the app.js file, the division into three logical parts clearly suggests itself:

- In the first part we take out all the logic of working with data models

- In the second part, all the auxiliary utilities, now we only have the function merge

- The third will be the server itself



We have decided on our desires, we will start writing tests, but first we will write a Makefile for the convenience of running the tests and place it in the project root.

Makefile
 MOCHA = ./node_modules/.bin/mocha test: @NODE_ENV=test $(MOCHA) \ -r should \ -R spec .PHONY: test 




- -r - indicates that Mocha should include the library should

- -R - indicates in what form we want to see test reports. There are several types of reports, this will look something like this:

Picture






Testing Tulses


By default, Mocha runs tests from the test directory, so we will create such a directory and write our first test.

And we will start testing with our "tools", we will make a small plan.

- In the "tools" should be a function merge

- The merge function should merge two objects into one

- And the object that is transmitted first must be expanded, the second object

- The function should not change the second object.



BDD tests in Mocha begin with the describe () block ; , the tests themselves are written in it () blocks ; they must be located inside the describe () block. Any nesting of describe () blocks in each other is allowed. Hooks are also available: before (), after (), beforeEach (), and afterEach (). Hooks must also be described inside the describe () block. I will tell you more about hooks when we test our models for working with fake databases.



In the test directory, create a file tools.js and write a test for tools.merge.

test / tools.js
 var tools = require('../lib/tools/index.js'); describe('tools', function () { // ""    merge it('should be have #merge', function () { tools.should.be.have.property('merge'); tools.merge.should.be.a('function'); }); describe('#merge', function () { // merge       it('should merged', function () { var a = { foo: '1' }, b = { bar: '2' }; tools.merge(a, b).should.eql({ foo: '1', bar: '2' }); }); //      ,   it('should be extend', function () { var a = { foo: '1' }, b = { bar: '2' }; tools.merge(a, b); //   ,    //     a.should.not.equal({ foo: '1', bar: '2' }); a.should.equal(a); }); //      it('should not be extended', function () { var a = { foo: '1' }, b = { bar: '2' }; tools.merge(a, b); b.should.not.eql({ foo: '1', bar: '2' }); }); }); }); 




If we start the test now, we will fall with an error even at the stage of connecting the tools module, which is normal, we don’t have this module yet. Create a file lib / tools / index.js and transfer the code of the function merge from lib / app.js there.

Run the make test and see that all four tests are inundated,

picture




because the very first test is filled up; it becomes clear that the function merge is not exported from the tools module. Add export and run the tests again, now everything should be fine.



Before proceeding with further testing of the rest of the application, add tests for coverage.

Let's add jscoverage launch with parameters --encoding = utf8 and --no-highlight specify lib as the incoming directory, and specify lib-cov as the outgoing directory. Now we ’ll add Mocha launch for coverage tests, set the COVERAGE = 1 environment variable as the reporter and specify html-cov to get a nice html page with the results of the coverage tests.

Makefile
 MOCHA = ./node_modules/.bin/mocha test: @NODE_ENV=test $(MOCHA) \ -r should \ -R spec test-cov: lib-cov @COVERAGE=1 $(MOCHA) \ -r should \ -R html-cov > coverage.html lib-cov: clear @jscoverage --encoding=utf8 --no-highlight lib lib-cov clear: @rm -rf lib-cov coverage.html .PHONY: test test-cov clear 




Let's return to our test and at the very top replace the line:

 var tools = require('../lib/tools/index.js'); 


on

 var tools = process.env.COVERAGE ? require('../lib-cov/tools/index.js') : require('../lib/tools/index.js'); 


Everything. Now we can run make test-cov . The coverage.html file will appear in the project root, with the results of the coverage test, the file is self-sufficient and can be immediately opened in the browser.

Picture




Red will be shown lines in which there was not a single approach, which means that this place is not covered by tests. It also provides general statistics of test coverage in percent for each file.



Great, the testing environment is set up, it remains to write tests for the database and the server.



We test work with base


We write the code to test our models. First, we define the functionality.

1) We should have two models User and UserList

2) The User model must have methods:

- find - the function returns a list of users with an object of type UserList, even if there is nothing

- findById - the function should search for the user by ID and return the result as a User object, or nothing if there is no user with this ID

- save - the function should save the user, returns err in case of an error

- toJSON - the function returns causes an object of type User to json

3) The UserList model should have only the toJSON method

Test code
 var should = require('should'), db = process.env.COVERAGE ? require('../lib-cov/models/db.js') : require('../lib/models/db.js'), models = process.env.COVERAGE ? require('../lib-cov/models/index.js') : require('../lib/models/index.js'), User = models.User, UserList = models.UserList; describe('models', function () { //      //   "describe('models')" before(function () { db.regen(); }); //    User it('should be have User', function () { models.should.be.have.property('User'); models.User.should.be.a('function'); }); //    UserList it('should be have UserList', function () { models.should.be.have.property('UserList'); models.UserList.should.be.a('function'); }); //  User describe('User', function () { // User    find it('should be have #find', function () { User.should.be.have.property('find'); User.find.should.be.a('function'); }); // User    findById it('should be have #findById', function () { User.should.be.have.property('findById'); User.findById.should.be.a('function'); }); // User    save it('should be have #save', function () { User.prototype.should.be.have.property('save'); User.prototype.save.should.be.a('function'); }); // User    toJSON it('should be have #toJSON', function () { User.prototype.should.be.have.property('toJSON'); User.prototype.toJSON.should.be.a('function'); }); describe('#find', function () { //find   UserList it('should be instanceof UserList', function (done) { User.find(function (err, list) { if (err) return done(err); list.should.be.an.instanceOf(UserList); done(); }); }); //find   UserList,     it('should not be exist', function (done) { //  db.drop(); User.find(function (err, list) { //  db.generate(); if (err) return done(err); list.should.be.an.instanceOf(UserList); done(); }); }); }); describe('#findById', function () { //findById     User it('should be instanceof User', function (done) { User.findById(0, function (err, user) { if (err) return done(err); user.should.be.an.instanceOf(User); done(); }); }); //findById   ,     it('should not be exists', function (done) { User.findById(100, function (err, user) { if (err) return done(err); should.not.exist(user); done(); }); }); }); describe('#save', function () { //save   ,     it('should not be saved', function (done) { var user = new User({ name: 'New user', age: 0, sex: 'w' }); user.save(function (err) { err.should.eql('Invalid age'); done(); }); }); //  ,       it('should be saved', function (done) { var newuser = new User({ name: 'New user', age: 2, sex: 'w' }); newuser.save(function (err) { if (err) return done(err); User.findById(newuser.id, function (err, user) { if (err) return done(err); user.should.eql(newuser); done(); }); }); }); }); describe('#toJSON', function () { //toJSON   json   it('should be return json', function (done) { User.findById(0, function (err, user) { if (err) return done(err); user.toJSON().should.be.eql({ id: 0, name: 'Jo', age: 20, sex: 'm' }); done(); }); }); }); }); describe('UserList', function () { //UserList    toJSON it('should be have #toJSON', function () { UserList.prototype.should.be.have.property('toJSON'); UserList.prototype.toJSON.should.be.a('function'); }); }); }); 




The code is supplied with comments, therefore I will stop only on the separate moments.

  before(function () { db.regen(); }); 


This code will be called once at the start of testing. Here you can connect to the database and fill in the test data, we do not have a real database, so we call only the regen method, which initializes our database with test data.

It is worth paying attention to the fact that the work with the database is carried out in an asynchronous style; when testing asynchronous methods, we must call the done () method after completing the testing of the block; A piece of code for clarity:

 ... //find   UserList it('should be instanceof UserList', function (done) { User.find(function (err, list) { if (err) return done(err); list.should.be.an.instanceOf(UserList); done(); }); }); ... 




Now we will start implementation. Let's create in the lib directory, the models directory, where our functionality for working with models will be implemented:

models / db.js
 /* *    */ //  var users = []; exports.users = users; //     exports.regen = function () { exports.drop(); exports.generate(); }; exports.drop = function () { //    , //      users.splice(0, users.length); }; exports.generate = function () { //  users.push({ id: 0, name: 'Jo', age: 20, sex: 'm' }); users.push({ id: 1, name: 'Bo', age: 19, sex: 'm' }); users.push({ id: 2, name: 'Le', age: 18, sex: 'w' }); users.push({ id: 10, name: 'NotFound', age: 18, sex: 'w' }); }; //   exports.generate(); 




models / user.js
 var util = require('util'), db = require('./db.js'), UserList = require('./userlist.js'), users = db.users; /* *   */ var User = module.exports = function User(opt) { this.id = users.length; this.name = opt.name; this.age = opt.age; this.sex = opt.sex; this.isNew = true; } /* *        */ function loadFromObj(obj) { var user = new User(obj); user.id = obj.id; user.isNew = false; return user; } /* *       */ User.find = function (fn) { var i, l = users.length, list; if (l) { list = new UserList(); for (i = 0, l; l > i; i += 1) { list.push(loadFromObj(users[i])); } } fn(null, list); }; /* *    id */ User.findById = function (id, fn) { var obj = users[id], user; if (obj) { user = loadFromObj(obj); } fn(null, user); }; /* *  */ User.prototype.save = function (fn) { var err; //    if (Number.isFinite(this.age) && this.age > 0 && this.age < 150) { if (this.isNew) { users.push(this.toJSON()); this.isNew = false; } else { users[this.id] = this.toJSON(); } } else { err = 'Invalid age'; } fn(err); }; User.prototype.toJSON = function () { var json = { id: this.id, name: this.name, age: this.age, sex: this.sex }; return json; }; 




models / userlist.js
 var util = require('util'); /* * UserList -  ,   Array */ var UserList = module.exports = function UserList() { Array.apply(this) } util.inherits(UserList, Array); UserList.prototype.toJSON = function () { var i, l = this.length, arr = new Array(l); for (i = 0; l > i; i += 1) { arr[i] = this[i].toJSON(); } return arr; }; 




models / index.js
 exports.User = require('./user.js'); exports.UserList = require('./userlist.js'); 






We will tweak the lib / app.js code by adding the User model connection to it and will carry out all work with users through it.

lib / app.js
 var ... User = require('./models/index.js').User, ... ... app.get('/users', function(req, res, next){ User.find(function (err, users) { if (err) { next(err); } else { res.render('index', { users: users.toJSON() }); } }); }); app.get('/users/profile', function(req, res, next){ var id = req.query.id; User.findById(id, function(err, user) { if (user) { res.render({ user: user.toJSON() }); } else { next('Not found'); } }); }); ... 






Testing the application


The last part not covered with tests remains. This is directly http server. Honestly, I decided to sham and test only four situations here:

1) The answer should come in html if this is a normal request.

2) The answer should come to json if it is an Ajax

3) GET request to the site root should return the page / object, where the title contains the value 'My site'

Thanks to the supertest library, writing such tests is easy and simple:

test / app.js
 var request = require('supertest'), app = process.env.COVERAGE ? require('../lib-cov/app.js') : require('../lib/app.js'); describe('Response html or json', function () { //   ,   //   html it('should be responded as html', function (done) { request(app) .get('/') .expect('Content-Type', /text\/html/) .expect(200, done); }); //  ,   json it('should be responded as json', function (done) { request(app) .get('/') .set('X-Requested-With', 'XMLHttpRequest') .expect('Content-Type', /application\/json/) .expect(200, done); }); }); describe('GET /', function () { //  title ===   it('should be included title', function (done) { request(app) .get('/') .end(function (err, res) { if (err) return done(err); res.text.should.include('<title> </title>'); done(); }); }); //  title ===   it('should be included title', function (done) { request(app) .get('/') .set('X-Requested-With', 'XMLHttpRequest') .end(function (err, res) { if (err) return done(err); res.body.should.have.property('data'); res.body.data.should.have.property('title', ' '); done(); }); }); }); 




In request (), we must pass an instance of http. Server or a function that executes the request. SuperTest uses SuperAgent to communicate with the server, so you can use all its capabilities to form requests to the server. Responses can be checked in expect () functions or directly as a result of a request by passing a handler function to end () .



Conclusion


It’s just so easy (and even necessary) to write tests for our applications. Even in my small sample code for tests, it turned out more than the code itself, but even these tests are not complete and, for example, the tests do not cover the error when we create two users at the same time and save them. Although coverage tests show that the User model is covered with tests in the place where the new user is saved.

Therefore, the tests themselves are not a panacea, the tests must be written correctly and you need to understand the little things, and test exactly the places that can cause problems.



Code is available on github'e

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



All Articles