How To Test Node Applications Using Bookshelf.js

Baruch Kogan
4 min readJan 9, 2019

I often find myself writing Node projects which use MySQL databases. I do not deal with the database directly, preferring to use an ORM. Generally, I prefer Bookshelf.js, which allows me to drop down into the Knex query builder and use its methods both for queries and for seeding and migrations as necessary. But how do you test methods which rely on an ORM?

One approach is using mocks. There is this great article by Joan Ortega on using Mock-Knex with Bookshelf. I found this approach to be difficult because you have to explicitly write each query response, which is tedious and fragile.

A different testing approach, which I found easy to implement, extend and reason about, and which very closely approximates what my code actually does in production, is to configure Bookshelf/Knex to use an in-memory SQLite database in a testing environment. We then run our migrations, to produce the same database structure that we see in production, and a test seed file, to provide realistic data. At this point, we can work with our database the same way that we do in production, testing all sorts of operations easily.

I was keyed onto this approach by Steven Hunt’s StackOverflow answer , and thought I would write this article explaining it in more depth. I’ll be using Daimonion, an authentication microservice I wrote with Node, Express, Bookshelf and Redis, for examples.

The first thing I do is create a db_config.json file to hold my connection details. In this file, I have two objects: “production” and “test.” “Test” holds the details for my in-memory SQLite connection, and “production” for my regular MySQL connection:

{
"production": {
"client": "mysql",
"connection": {
"host": "localhost",
"user": "root",
"password": "password",
"database": "daimonion-db"
}
},
"test": {
"client": "sqlite3",
"connection": ":memory:",
"pool": {
"min": 1,
"max": 1,
"disposeTimeout": 3600000
},
"useNullAsDefault": true
}
}

db_config.json

Next, I create my Bookshelf and Knex connection instances and export them. Depending on my environmental variables, I either require the test or production config object.

'use strict';
let dbConfig, knex;

if (process.env.NODE_ENV === 'test') {
dbConfig = require('../config/db_config')['test'];
knex = require('knex')(dbConfig);
knex.processFlag = 'test';
}
else{
dbConfig = require('../config/db_config')['production'];
knex = require('knex')(dbConfig);
}

const jsonColumns = require('bookshelf-json-columns');

const bookshelf = require('bookshelf')(knex);
bookshelf.plugin(jsonColumns);
bookshelf.plugin('pagination');
bookshelf.plugin('registry');

module.exports.bookshelf = bookshelf;
module.exports.knex = knex;

bookshelf_instance.js

Now, we require migrations to create our database structure. I only have one migration here. Knex allows us to use ES6 with async-await, making for nice, readable migrations.

'use strict';
exports.up = async function (knex) {
try{
const hasUser = await knex
.schema
.hasTable('user');

if (!hasUser)
await knex
.schema
.createTable('user', table => {
table.increments('id').primary().unsigned();
table.string('first_name');
table.string('last_name');
table.string('email');
table.string('phone');
table.string('password').notNullable();
table.string('username').notNullable();
});

const hasToken = await knex
.schema
.hasTable('token');

if (!hasToken)
await knex
.schema
.createTable('token', table => {
table.increments('id').primary().unsigned();
table.string('token');
table.dateTime('expires');
table.integer('user_id').unsigned().notNullable();
table.foreign('user_id').references('user.id')
.onDelete('CASCADE')
.onUpdate('CASCADE');
});
}
catch (e) {
console.log('knex migrations failed with error ', e);
}
};

exports.down = async function (knex) {
await knex.schema
.dropTable('token')
.dropTable('user')
};

initial_migration.js

And let’s make a seed file to provide us with some data to play with:

exports.seed = async (knex) => {
try {
if (knex.processFlag !== 'test')
return;
// Deletes ALL existing entries
await knex('user').del();

await knex('user').insert([
{
id: 1,
first_name: 'Ploni',
last_name: 'Almoni',
email: 'plonialmoni@gmail.com',
phone: '123456789',
password: `password`,
username: 'MyLittlePloni'
}
]);

await knex('token').del();

await knex('token').insert([
{
id: 1,
token: '12345',
expires: '2021-01-01 00:00:00',
user_id: 1
}
]);
}
catch (e) {
console.log('test seed failed with error ', e);
}
};

Notice that we make sure that the seed file only runs if the knex processFlag property is set to ‘test’, which we did in the bootstrap file above. This is an extra precaution to prevent accidentally contaminating our production database with testing data.

Now, in our test file, we can run our migrations and seed file before running our tests. In this case, I am not testing against the seed data, but I could if I wanted to.

process.env.NODE_ENV = 'test';
const expect = require('chai').expect;
const app = require('../app');
const knex = require('../bootstrap/bookshelf_instance').knex;
const request = require('supertest');

const registerCredentials = {
username: 'Test',
password: 'Pass1234',
password_confirm: 'Pass1234',
email: 'test@test.com',
first_name: 'TestFirstName',
last_name: 'TestLastName',
};

const loginCredentials = {
username: 'Test',
password: 'Pass1234',
};

let testToken;


before(async function (done) {
await knex.migrate.latest();
await knex.seed.run({directory: './seeds'});
done();
});

describe('/POST /api/authentication/register', function() {
it('should return a 200 response if we pass proper registration credentials',
function (done){
request(app)
.post('/api/authentication/register')
.send(registerCredentials)
.expect(200, done)
});
});

describe('/POST /api/authentication/login', function() {
it('should return a 200 response if we pass proper login credentials',
function (done){
request(app)
.post('/api/authentication/login')
.send(loginCredentials)
.end(function(err, response){
expect(response.statusCode).to.equal(200);
testToken = response.body.token;
done()
});
});
});

describe('/POST /api/authentication/authenticate', function() {
it('should return a 200 response if we pass a proper token',
function (done){
request(app)
.post('/api/authentication/authenticate')
.send({token: testToken})
.expect(200, done)
});
});

authentication-controller.js

The nice thing about this approach is that in any particular test, I could reach into the database, either using Bookshelf or Knex, and see that the data changes my code is supposed to be making are actually happening.

A couple of minor problems to watch out for stem from differences between SQLite and MySQL which bubble up to Knex and Bookshelf. If, in the course of your migrations, you find yourself adding foreign keys to an previously created table, or removing them from such a table, these migrations will fail when run against SQLite. Refactor them so that the foreign relationships are created in the initial table definition. Also, we are using the Bookshelf JSON Columns plugin; this works a bit strangely with SQLite sometimes, and you may have to call JSON.parse() or JSON.stringify() on certain values in your tests in order to deal with this.

I hope you found this article useful. If you have any suggestions or feedback, please drop me a line in the comments.

--

--

Baruch Kogan

Settler in the Shomron. Tech/manufacturing/marketing/history.