Loading...
In this lesson we will learn how to unit test our express application. We will talk about writing unit tests with Mocha We will learn how to mock our data, and how to test our views and rest server.
I've seen alot of companies who don't believe in testing their code Apperantly it takes too long, it's hard to maintain, and it's just plain and simple does not worth the effort. But in fact writing tests is actually a time saver. It's easy to maintain if you know how to write tests that won't break. And it does worth the effort cause you product will mover quicker from development to production, refactoring of your product code will be quicker, and the quality of the product will be much higher with less bugs. The only thing it takes is understand how to write tests properly. Once you practice writing tests it becomes somtime quicker then checking all the previous features with your browser. Plus if you discover a bug and write a test to catch it, you will know for a certainty that you not only solved the bug, but also that this bug won't return in the future. So although most of the developers will prefer working on features or debugging the code and writing tests is a bit less glamorous, it's well worth the effort.
If you know what to test, or rather know what not to test, It's less likely that you tests will break often and you will need to maintain them less.
So there are a few rules that you have to maintain while testing:
- the tests should not depend on external resources. For example there are cases where we want to test our rest server and we are using a database in our test.
Database is an external source that does not rely on the test, on first run of the test the data could be equal to something and the test will pass, on the second run of the test, sombody altered the data and now the tests fails.
This means that our test data should be mocked.
- test functionality and not appearance.
Our application may change the styling and appearance often, so focus you tests on functionality.
In general testing the model section of your app, tends to bring much stabler tests then testing the views.
This does not mean you should not the your views rather be carfull on what you test of the views.
If you are testing your login do not query the form inputs with some css class that might change, rather prefer query the input with something more solid like the name attribute, or the form.
- If you discovered a bug which you describe in severity level as critical, you have to add a test to discover this bug, this way the bug won't return in future releases.
- In general if you do devide to write tests for your app, the next step will be to combine your development, build, deploy with a CI tool like Jenkins or Travis. Writing tests without combining them with a CI tool makes the tests basically worthless, you will soon realize that you forget to run them before deploying your code and if you are not running them they will soon be absolete.
Make sure running the tests is automated in your deployment process.
In this lesson we will practice different kind of testing. First the easiest and most effective unit tests, we will test our services (the business logic, in MVC this would be the M part). Second we will learn how to test our views, and finally we will learn how to test our rest server.
Create a directory for you project called express-testing. In that directory we will init npm and install mocha.
> mkdir express-testing
> cd express-testing
> npm init --yes
> npm install mocha --save-dev
Create a directory for our application services, in that directory we will create a service called email-utils.js This module will export a singleton class with a single method that will get a string argument, and return true if that string is a valid email address. the code in the email-utils.js file should be this:
let instance = null;
module.exports = class EmailUtils {
static getInstance() {
if (instance) {
return instance;
}
instance = new this();
return instance;
}
isEmailValid(email) {
const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
}
in order to make the class singleton, we create an instance variable and when populated we will return that instance. Now we want to create a test for the service we created above, and we want to test that the method isEmailValid would indeed work and return true on a valid email and false on an invalid email. To do this we first need to understand how to write tests in Mocha
Mocha is a test framework that runs on node. Using mocha you can create a group of tests, and inside that group create either subgroups or tests. In each test you can create number of asserts that if fails the test will fail. in each group of tests you can create hooks that will run before or after each test. When mocha is running the tests you have a few global methods you can use, lets go over those methods
describe is a function that you call to create a group of tests. the first argument you pass to this function is the name of the group of tests, the second argument to the function is a function where you create your tests. a group of describe can host subgroup so you can nest subgroup of tests together. in the function we give in the second argument this will refer to Mocha object where you can configure additional options for running this group of tests, for example setting the timeout for tests of this group. using the describe looks something like this:
describe('describe test group', function() {
// .... write the tests of this group
});
Make sure the second argument of the describe is not a lambda function otherwise you will loose this pointing to mocha object
with this function you can write a single test. it will be placed inside a describe block. it is a function which gets a string as first argument describing what we are testing, the second argument is a function to write the test in. In the test function we are doing assertions and expectation, mocha can work with node built in assertion library or with other popular community expect libraries. We recomnmend checking out chai as an assertion library, but in this tutorial we will use the built in node assert library. the test function can also be an async function and can get a done argument to call to end the test. The test function can also be an async await function.
Mocha lets you write hooks that will run before or after you tests. You place those hooks inside a describe block and they will run before or after the tests in the group or the subgroups in that group. before will run once in that group before running any test. beforeEach will run before every test in that group after will run after all the tests finished afterEach will run after every test.
Now that we understand how mocha works, it's time to write our first test and test the service we created email-utils.js
We will now write a test for the service we created. We created a singleton service with a method to verify that a string is a valid email address. The test we will create will varify that the isEmailValid works and we will try that method with different string values. In the services folder, create a file called: email-utils.spec.js and place the following code:
const EmailUtils = require('./email-utils');
const assert = require('assert');
describe('email-utils', function() {
let emailUtils;
before(function() {
emailUtils = EmailUtils.getInstance();
});
it('isEmailValid', function() {
assert(emailUtils.isEmailValid('yariv') === false);
assert(emailUtils.isEmailValid('') === false);
assert(emailUtils.isEmailValid('adsfgsdfg@sdfsd.com') === true);
});
});
lets go over the simple test we wrote here.
- We imported the service we want to test as well as the assert library.
- we placed our tests in a describe group.
- we run a before hook once before all the tests, since our service is a singleton we can instantiate it once so we do it once in the before hook.
- we place the test in an it block and use the assert library to make expectation when running our function.
Now we can tell mocha to run our test with the following command:
> mocha **/*.spec.js
basically mocha will search in every folder for files that end with spec.js
We saw how easy it is to write tests for our services, lets makes things a bit harder and test our views. We will create a login page with a form, where the user needs to supply a valid email to log in. When mail is invalid he will be directed to the login page with an error message. If mail is correct we will direct him to the welcome in page. We would like to write a test for this page and verify with a test that it is working properly.
Lets start with creating the login with express. Lets install some needed packages to start an express application, and create a view. In the terminal type:
> npm install express pug body-parser --save
Lets create a file called app.js and bootstrap express in that, create a route for the login page, create a route for the welcome page, and deal with the post of the login form. Add the following code in app.js
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser');
const EmailUtils = require('./services/email-utils');
const app = express();
app.set('view engine', 'pug');
app.set('views', path.resolve(__dirname, 'views'));
app.use(bodyParser());
app.route('login')
.get(function(req, res) {
res.render('login', {error: req.query.error});
})
.post(function(req, res) {
const email = req.body.email;
const emailUtils = EmailUtils.getInstance();
if (emailUtils.isEmailValid(email)) {
res.redirect('welcome');
} else {
res.redirect('login?error=invalid');
}
});
app.listen(3000, function() {
console.log('We are now listening on port 3000');
});
Notice that we are creating two routes here.
- login route when a get request is recieved we are rendering the login template, when a post is sent we are checking if the email is valid and redirecting to the welcome page if valid or to the login with a query param of error.
- the second route we have here is for the welcome page.
We will also need to create the two templates for the login and the welcome.
Create a directory in the project directory called views.
In that directory create a file called login.pug with the following:
doctype html
html
head
title login page
body
h1 welcome to the login page
form(method="post" action=".")
div
label email
input(type="text" name="email" placeholder="email")
- if(error === 'invalid')
div(class="alert alert-danger") Invalid Email
div
button(type="submit") Login
This template will render a form on the screen. Notice also that there is an optional error message on the bottom of the form, if the error variable is passed from the express side then we will show the error. Now lets create the template for the welcome page, in the views directory create a file called welcome.pug with the following:
doctype html
html
head
title Welcome Page
body
h1 You are now logged in
This template when rendered will simply display a welcome message.
Time to write a test and verify that our routes are working. The first test we will write is a valid mail test. We will enter a valid mail in the form and verify that we are directed to the welcome page. The second test we will write is for an invalid email, we will verify the on invalid mail we are directed to the login page with an error message. Create a file called login.spec.js with the following code:
require('./app');
const http = require('http');
const assert = require('assert');
const cheerio = require('cheerio');
describe('login page', function() {
it('get login', function(done) {
const req = http.get('http://localhost:3000/login', function(res) {
let html = '';
res.on('data', function(d) {
html+=d;
});
res.on('end', function() {
const $ = cheerio.load(html);
assert($('form').length === 1);
assert($('alert-danger').length === 0);
done();
})
});
req.end();
});
it('get login with error flag', function(done) {
const req = http.get('http://localhost:3000/login?error=invalid', function(res) {
let html = '';
res.on('data', function(d) {
html+=d;
});
res.on('end', function() {
const $ = cheerio.load(html);
assert($('form').length === 1);
assert($('.alert-danger').length === 1);
done();
})
});
req.end();
});
it('success login', function(done) {
const req = http.request({
method: 'POST',
hostname: 'localhost',
port: 3000,
path: '/login',
headers: {
'Content-Type': 'application/json'
}
}, function(res) {
assert(res.statusCode === 302);
assert(res.headers.location === 'welcome');
done();
});
req.write(JSON.stringify({
email: 'no@no.com'
}));
req.end();
});
it('failed login', function(done) {
const req = http.request({
method: 'POST',
hostname: 'localhost',
port: 3000,
path: '/login',
headers: {
'Content-Type': 'application/json'
}
}, function(res) {
assert(res.statusCode === 302);
assert(res.headers.location === 'login?error=invalid');
done();
});
req.write(JSON.stringify({
email: 'nodxfgvsxzcv'
}));
req.end();
});
});
Ok so quite a few tests examples in this file.
First of all, notice that on the top of the file we are requiring the express application app.js in order to launch our server.
Our tests are simply performing the different option for requests that are possible.
First we do a get request to the login page (regular get request) and we verify that the response contains a form, and the response does not render with an error. We are using cheerio which we can give a string of an html and query the html document like jquery.
Second request is for the login page with a query param of error and we verify that an error message is rendered.
Notice that our tests are async now since we are sending an async http request, this means that we need to tell mocha when the test is ending by calling the done method which we get on the test function.
The 3rd and 4th test we are sending a post request with a valid and invalid email, we verify the location of the redirect we are getting in both cases.
In our login route that we now tested, there is a usage of the email-utils.js service. In our case the service is quite simple and is non dependent on api that might change data from one run of the test to another. What is the service in the route was depending on a database query, this means that the test result might change if the database change. We also want the test to focus on the view and not on the services which we already wrote tests for. What we would rather do in this case is create a stub for the email-utils.js. The idea is fake our service to a stupid service that would simply return a result based on what we want to recieve in the test. To understand how to create a stub we need to familiarize ourselves with another library.
Sinon is a library we can use for stubs, spies, and mocks. Spies are used to check if a method is called, or how many times its called or with what arguments. Mocks are used to replace an entire object or function with a replacement. Lets see how we can achieve our same tests while using sinon to mock and to stub our service. First Lets install sinon with npm:
> npm install sinon --save-dev
We will start by stubbing our service and make it return true or false based on what we need in the test. Modify the login.spec.js to look like this:
require('./app');
const http = require('http');
const assert = require('assert');
const cheerio = require('cheerio');
const sinon = require('sinon');
const EmailUtils = require('./services/email-utils');
describe('login page', function() {
function fakeIsValidEmailTrue() {
const fake = sinon.fake.returns(true);
sinon.replace(EmailUtils.prototype, 'isEmailValid', fake);
}
function fakeIsValidEmailFalse() {
const fake = sinon.fake.returns(false);
sinon.replace(EmailUtils.prototype, 'isEmailValid', fake);
}
afterEach(function() {
sinon.restore();
})
it('success login', function(done) {
fakeIsValidEmailTrue();
const req = http.request({
method: 'POST',
hostname: 'localhost',
port: 3000,
path: '/login',
headers: {
'Content-Type': 'application/json'
}
}, function(res) {
assert(res.statusCode === 302);
assert(res.headers.location === 'welcome');
done();
});
req.write(JSON.stringify({
email: 'no@no.com'
}));
req.end();
});
it('failed login', function(done) {
fakeIsValidEmailFalse();
const req = http.request({
method: 'POST',
hostname: 'localhost',
port: 3000,
path: '/login',
headers: {
'Content-Type': 'application/json'
}
}, function(res) {
assert(res.statusCode === 302);
assert(res.headers.location === 'login?error=invalid');
done();
});
req.write(JSON.stringify({
email: 'nodxfgvsxzcv'
}));
req.end();
});
it('get login', function(done) {
const req = http.get('http://localhost:3000/login', function(res) {
let html = '';
res.on('data', function(d) {
html+=d;
});
res.on('end', function() {
const $ = cheerio.load(html);
assert($('form').length === 1);
assert($('alert-danger').length === 0);
done();
})
});
req.end();
});
it('get login with error flag', function(done) {
const req = http.get('http://localhost:3000/login?error=invalid', function(res) {
let html = '';
res.on('data', function(d) {
html+=d;
});
res.on('end', function() {
const $ = cheerio.load(html);
assert($('form').length === 1);
assert($('.alert-danger').length === 1);
done();
})
});
req.end();
});
});
We are using sinon fake which combines usage from spies and stubs on a method. With fake you can create a function and use sinon.replace to make that function replace an existing function. You can also use the fake to know information about how many times the function is called with what argument and more data on the call of the function. Think of fakes as something that combine spies (which gives data on a call of a function) and stubs which replace a function with a different one. Lets explain the code above. We create two inner functions: one that creates a truty fake and replace the isEmailValid, and one that creates a falsy fake and replace the isEmailValid. It's not necesary for us to create an instance of the class EmailUtils and through the access of the prototype we can replace the method from the class and not from the instance. If we had an instance then the replace would be without the prototype, in this case since its a singleton we can create an instance and fake the method through the instance (all tests will use the same instance), but if the class is not a singleton we need to replace the method from the prototype. Notice also that to restore the original usage of the EmailUtils class after each class we call sinon.restore() to remove our fake and return the service to its original use. We call the inner function on the tests we want to fake the service. You can verify that its working by sending a request with different emails and see that the result will remain the same.
The last section of this lesson is how do we test a REST API which relies on a database.
The main purpose of this section is to understand how we combine our tests with database usage properly.
On one hand we said the tests can not relay on a 3rd party data that might change from run A to run B, but on the other hand we do want to test our app while its connected to a database, we want to verify that our database queries are correct and we will prefer to use the database api rather that starting to stub all the methods of the api that we are using.
So to still use the database in our tests we will require the following:
- before each test clean the database so we will have a fresh start on every test
- before each test load fixed and know data in the database so the data will be consistent between every run - These are refered to as fixtures.
Before we practice using fixture, lets first create our rest api. We will use postgres and connect to a local database, and we will use sequelize as our ORM.
This article will not go over about how to connect our express application with postgres and use sequelize, so we recommend reading the previous lesson about it.
We will create a users table with an email column.
We will create a get api to get all our users, and a post api to create a new user in the database.
First we will not to use npm to install sequelize and to install the library the sequelize use to connection to postgres.
In the terminal using npm install the following:
> npm install sequelize pg pg-hstore --save
We will also need in our terminal to create a database cluster, init the database server on that cluster. We will also create a database for our app called testing and an additional database used for running our tests called stam In the terminal do the following:
> initdb /abs/path/to/where/to/save/db/cluster -E utf8
> pg_ctl -D /abs/path/to/where/to/save/db/cluster -l logfile start
> createdb testing
> createdb stam
lets first create the sequelize model for the users table. Create a folder in the root project called models and in that file add a file called user.js with the following code:
module.exports = function(sequelize, DataTypes) {
const User = sequelize.define('user', {
email: {
type: DataTypes.STRING,
validate: {
isEmail: true
}
}
});
User.sync();
return User;
}
We created here an ORM to map to the users table. And we are syncing the sequelize model so it will create the table if it doesn't exist. The table will contain one column for the mail of the users
Lets add the API to get all the users and to create a new user in the table. At the bottom of the file app.js just before the call to app.listen add the following code:
const Sequelize = require('sequelize');
const sequelize = new Sequelize(process.env.DATABASE_URL);
const User = sequelize.import('./models/user');
app.route('/users')
.get(function(req, res) {
User.findAll()
.then(users => res.json(users));
})
.post(function(req, res) {
User.create(req.body)
.then(user => res.json(user));
});
In this code we are doing the following:
- Creating a connection to the database, we are doing the creation with an environment variable, which means that when we are running our tests we can specify an environment variable to connect to the stam database we created rather then the database we regularly use. This will cause the test not to leave sideeffects on a database that might be used in production.
- we create a get route for getting all the users, and a post route to create a new user.
Lets write a test with database fixtures on the code we written.
Lets add a test to check the users api.
This test we will run with an environment variable DATABASE_URL=postgres://localhost:5432/stam
Which is a special database for our testing.
Every run of the tests we will clear the data in the users table and create a fixed data.
Add a file called users.spec.js with the following code.
require('./app');
const Sequelize = require('sequelize');
const sequelize = new Sequelize(process.env.DATABASE_URL);
const User = sequelize.import('./models/user.js');
const http = require('http');
const assert = require('assert');
describe('users API', function() {
// clear the users table
beforeEach(async function() {
await User.destroy({where: {}});
});
// insert some users
beforeEach(async function() {
await User.bulkCreate([
{email: 'test1@test.com'},
{email: 'test2@test.com'},
{email: 'test3@test.com'},
])
})
it('should return 3 users', function(done) {
http.get('http://localhost:3001/users', function(res) {
let html = '';
res.on('data', d => html+=d);
res.on('end', () => {
const obj = JSON.parse(html);
assert(obj.length === 3);
done();
});
})
});
it('should create a new user with id 4', function(done) {
const req = http.request({
host: 'localhost',
port: 3001,
method: 'post',
headers: {
'CONTENT-TYPE': 'application/json'
},
path: '/users'
}, function(res) {
let html = '';
res.on('data', d => html+=d);
res.on('end', () => {
const obj = JSON.parse(html);
assert(obj.email === 'stam@stam.com');
done();
});
})
req.write(JSON.stringify({email: 'stam@stam.com'}));
req.end();
});
})
Few things to note here:
- The first before each will delete all users record - you can pass an async function in the before each, which will work perfectly with the fact that sequelize works with promises.
- the second before each will create 3 dummy users.
- The first test will issue a get request and verify that we are getting back 3 users.
- The second test will create a user and verify that the created user is returned.
The important thing to note here is that we are using a seperate db for testing.
We are keeping our data persistent between tests by cleaning the db and loading data.
Testing is an important part of building a strong reliable program that we must master in order to write better quality applications. Once getting used to writing tests it will come natural and will be quicker then checking it works in the browser. Happy Coding :)