What I want to do
I'm trying to test a node.js
function which uses knex
.
Instead of just mocking knex
, I think it's interesting to actually run the test on an in-memory database, which makes this test not strictly unitary, but it's, to me, the only useful way to test a repository
class.
It's also the most voted answer here: https://stackoverflow.com/a/32749601/1187067
What I use in my test
- A simplified book repository
bookRepo.js
based onknex
- A test
bookRepo.test.js
injecting aknex
connection based onSQLite3
.
Problem
The database is well initialize, the test succeeds and the afterEach()
function is well invoked, but the process never ends which is particularly problematic for pipelines.
The only way I found to stop the process is to call knex.destroy()
in both bookRepo.js and bookRepo.test.js, but it is not possible to destroy knex because it won’t be possible to use it more than once.
Thanks for helping!
Code
bookRepo.js
const knex = require('connection'); // dependency not injected in constructor
const TABLE = 'books';
class BookRepo {
constructor() {
this.knex = knex;
}
static getTable() {
return TABLE;
}
async getTitleById(id) {
const book = await this.knex(TABLE)
.select('title')
.where('id', id)
.first();
return book.title;
}
}
module.exports = BookRepo;
bookRepo.test.js
const { assert } = require('chai');
const mock = require('mock-require');
const {
describe,
it,
before,
after,
beforeEach,
afterEach,
} = require('mocha');
const sqliteConf = {
client: 'sqlite3',
connection: {
filename: ':memory:',
},
useNullAsDefault: true,
};
const knex = require('knex')(sqliteConf);
const booksTable = BookRepo.getTable();
const BOOK_1 = {
id: 1,
title: 'Batman',
};
let bookRepo;
describe('getTitleById', () => {
before(() => {
// To use sqlite3 in the tested module, replace knexfile (required by connections)
mock('../knexfile', {
development: sqliteConf,
});
// as knex is not injected in constructor, we need to require BookRepo after having mocked knexfile.
const BookRepo = require('../bookRepo');
bookRepo = new BookRepo();
});
after(() => {
mock.stopAll();
knex.destroy(); // destroys only the connection of the test, not in bookRepo
});
/**
* We initialize the SQLite database before each test (create table and insert)
*/
beforeEach(async () => {
// drop table
await knex.schema.dropTableIfExists(booksTable);
// create table
await knex.schema.createTable(booksTable, (table) => {
table.integer('id');
table.string('title');
});
// Insertion
await knex.transaction((t) => knex(booksTable)
.transacting(t)
.insert(BOOK_1)
.then(t.commit)
.catch(t.rollback))
.catch((e) => {
console.error(e);
throw new Error('failed to insert test data');
});
});
/**
* We drop the SQLite table after each test
*/
afterEach(async () => {
await knex.schema.dropTableIfExists(booksTable); // table well dropped
});
it('returns the title of the given book', async () => {
const bookRepo = new BookRepo();
const expectedTitle = BOOK_1.title;
const retrievedTitle = await bookRepo.getTitleById(BOOK_1.id);
assert.equal(retrievedTitle, expectedTitle); // OK
});
});
package.json
…
dependencies": {
"knex": "^0.20.1",
},
"devDependencies": {
"chai": "latest",
"mocha": "^6.2.2",
"sqlite3": "latest",
}
}