Pleymor
Pleymor

Reputation: 2921

Test of node.js method using knex with sqlite3 never stops

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

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",
  }
}

Upvotes: 1

Views: 1413

Answers (1)

Rich Churcher
Rich Churcher

Reputation: 7664

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.

Upvotes: 1

Related Questions