1

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 on knex
  • A test bookRepo.test.js injecting a knex connection based on SQLite3.

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",
  }
}
Pleymor
  • 2,611
  • 1
  • 32
  • 44

1 Answers1

1

Since you're using mocha, it seems best to use Mocha's after hook and call destroy from there. Likewise, you could instantiate Knex in before. I can't think of a reason you would need to call destroy in your non-test code.

Rich Churcher
  • 7,361
  • 3
  • 37
  • 60
  • Thank you, yes you are right, I can actually call destroy in the after hook and it works here since knex is injected to the bookRepo. Actually I should have required knex in the bookRepo as well, which is the exact situation I need to answer to. In this situation destroy in after hook is not enough unfortunately. However, it's still possible to add a `setKnex(knex)` in bookrepo to allow injection in tests. – Pleymor Nov 01 '19 at 10:40
  • 1
    You can also inject with a default if that's your preference, so `thingThatNeedsKnex(db = realConnection)`, and if you pass in a connection it'll use that instead of your standard one. – Rich Churcher Nov 02 '19 at 01:43