Knex, Bookshelf, mocks and Unit Tests

Joan Ortega
5 min readApr 21, 2016

--

This is my second story about Knex and Bookshelf. This time I’ll talk about mocking these dependencies for unit test. As it happened to me before, there’s a lack of information and documentation on internet about this subject. To start with, let’s suppose we have these two tables with this basic relationship:

Tables user and UserGroup

You can suppose that there are two files for each table (I’ll call them DAO files), defining the relationship via hasMany and belongsTo bookshelf methods.

// dao/user.js
require('./userGroup');
const user = bookshelf.Model.extend(
{
tableName: 'user',
userGroup: function () {
return this.belongsTo('UserGroup', 'userGroupId');
}
},
{
byIdWithUserGroup: function (userId) {
return this.forge()
.query({ where: { id: userId } })
.fetch({ withRelated: ['userGroup'] })
;
}
}
);

module.exports = bookshelf.model('User', user);
// dao/userGroup.js
require('./user');
const userGroup = bookshelf.Model.extend(
{
tableName: 'user_group',
users: function () {
return this.hasMany('User', 'userGroupId');
}
},
{}
);

module.exports = bookshelf.model('UserGroup', userGroup);

As we know (if you’re not familiar with it, I will tell you), for each DAO file a knex instance is required to init bookshelf and then create the model definition:

var knex = require('knex')({
client: 'mysql',
connection: process.env.MYSQL_DATABASE_CONNECTION
});
var bookshelf = require('bookshelf')(knex);

However, in lots of examples, this instance is always bound to a dev database. In unit test mode, I believe that we should have no connexion, queries should be mocked. You can find some advice that encourages you to use a light database like sqlite3 for test mode (see here). It allows us to take advantage of knex migrations to create the schema, and of seeds to populate data before each test. I believe they have real arguments to defend this idea. But, what if you still want to mock?

Enter mock-knex

As always, in the node community, there are some little, young libraries to help you. mock-knex is the first reference that you can find in forums, however their documentation and examples are really poor. After hours of failing attempts to set it up and make it work, I finally found an understandable way to play with it.

First, you need an additional knex config for test. The best way is to put it in the same file having running time instance (let’s call it db.js):

'use strict';

const knex = require('knex');
const mockKnex = require('mock-knex');
let connection;

if (process.env.NODE_ENV === 'test') {
connection = knex({ client: 'mysql', debug: false });
mockKnex.mock(connection, 'knex@0.10');
} else {
connection = knex({
client: 'mysql',
debug: true,
connection: process.env.MYSQL_DATABASE_CONNECTION
});
}

module.exports = connection;

You don’t need a real connexion to a database to define your models. So in test mode, there will be no reads (neither writes? — I haven’t tested mocking inserts and deletes via mock-knex yet — ). Let’s suppose that our service to test is this:

// userService.js
const
userDao = require('./dao/User');
module.exports.findAll = () => {
return userDao.fetchAll(); //fetchAll: base method on any model
};

module.exports.find = (userId) => {
return userDao.byIdWithUserGroup(userId);
};

Having mocked knex configuration for test with mock-knex, we can get a tracker object. This will allow us to stub the database queries. The syntax to use this object has 3 steps:

  • Recover tracker object
  • Install tracker
  • Define data to be returned when a sql query will be intercepted
const tracker = require('mock-knex').getTracker();
tracker.install();
tracker.on('query', (query) => {
query.response(MOCKED_DATA);
});

First Unit Test

Let’s test the UserService findAll function. For this, the basic Bookshelf Model method fetchAll should be mocked. We’ll suppose that there are 3 users in the database, and they should be all returned. For this, tracker should intercept the sql query made by Bookshelf.

'use strict';
const mockKnex = require('mock-knex');
const chai = require('chai');
const tracker = mockKnex.getTracker();
const expect = chai.expect;
const userService = require('../userService')

describe('Test DAO library with mock-knex', () => {

tracker.install();

describe('When calling userService.findAll', () => {
before(() => {
tracker.on('query', (query) => {
const results = [
{
id: 1,
firstName: 'A',
lastName: 'A',
email: 'a.a@mail.com'
},
{
id: 2,
firstName: 'B',
lastName: 'B',
email: 'b.b@mail.com',
esp: 1
},
{
id: 3,
firstName: 'C',
lastName: 'C',
email: 'c.c@mail.com'
}
];
query.response(results);
});
});

it('should return 3 users', () => {
return userService.findAll()
.then((res) => {
const users = res.toJSON();

expect(users).to.have.property('length', 3);
expect(users[0]).to.have.property('esp', false);
expect(users[1]).to.have.property('firstName', 'B');
expect(users[2]).to.have.property('lastName', 'C');
})
;
});
});

I run the test with npm test (I’m using mocha, so in my package.json, on the scripts block is a simple “test”: “NODE_ENV=test mocha”) and it works. If I modify the debug property on the test knex configuration, I can see the query printed on the output.

And things become tricky…

Now let’s test the second method UserService.find. To do this, we need to mock userDao.byIdWithUserGroup. This function loads the userGroup nested attribute. To do that, —sadly — Bookshelf makes two sql queries: the first will recover the user with the id passed as parameter. The second will search the userGroup based on the userGroupId stored on the first query retrieved user. Having debug active on knex config, we can easily see

It means that if we want to use mock-knex, we should mock also two queries. Having this in mind, my second tests is…

describe('When calling userService.find - additional query on userGroup', () => {
before(() => {
const results = [
{
id: 1,
firstName: 'A',
lastName: 'A',
email: 'a.a@mail.com',
userGroupId: 3
},
{
id: 3,
name: 'group3'
}
];

tracker.on('query', (query) => {
query.response([results.shift()]);
});
});

it('should return an user with its userGroup data', () => {
return userService.find(5)
.then((res) => {
const user = res.toJSON();
expect(user).to.be.an('object');
expect(user).to.have.property('firstName', 'A');
expect(user).to.have.property('userGroup');
expect(user.userGroup).to.have.property('name', 'group3');
})
;
});
});

Results:

Conclusion, if there can be one…

After all the fail attempts to make mock-knex work, I’m finally happy to see the results.

Now I know that mock-knex intercepts any query and you must be careful in how you set up the data to be returned. Otherwise, mock-knex will always return the same data for any query and for some cases, it doesn’t make any sense.

Finally, I’m very disappointed to find this multiple queries made by Bookshelf instead of handling table joins. As a result, we have to know the order of the queries executed if we’re using the withRelated property to fetch embedded data. In our case, this problem made us uninstall Bookshelf and use directly knex.

--

--