Search code examples
node.jsunit-testingmocha.jssinon

Mocking es6 class constructor attribute with mocha/sinon


I have a small wrapper class which adds promises to some mysql functionality.

const mysql = require('mysql');


export default class MySQL {
    constructor(host, user, password, database, port = 3306) {
        this.conn = mysql.createConnection({
            host,
            port,
            user,
            password,
            database,
        });
    }

    query(sql, args) {
        return new Promise((resolve, reject) => {
            // eslint-disable-next-line consistent-return
            this.conn.query(sql, args, (err, rows) => {
                if (err) {
                    reject(err);
                    return;
                }
                resolve(rows);
            });
        });
    }

    close() {
        return new Promise((resolve, reject) => {
            this.conn.end((err) => {
                if (err) {
                    reject(err);
                    return;
                }
                resolve();
            });
        });
    }
}

I am trying to write a unit test for this class but am completely stuck trying to mock this.conn.

I have tried various mixes of proxyquire, sinon, and both combined. When I use proxyquire in a beforeEach hook:

beforeEach(function () {
    createConnectionStub = sinon.stub();
    MySQL = proxyquire('../../lib/utils/mysql', {
        mysql: {
            createConnection: createConnectionStub,
        },
    }).default;
});

and try to set a stub to the conn object:

it('Returns query results', async function () {
            stubDb = new MySQL('host', 'user', 'password', 'database');
            stubDb.conn = sinon.stub();

            const results = await stubDb.query('SELECT * FROM whatever');
        });

I keep getting TypeError: this.conn.query is not a function

what is the best way to setup a mock as the this.conn attributes so I can easily assert method calls against it? Any help would be much appreciated


Solution

  • I am an hour late. :)

    But I have coded the example and provide alternative to test, so I continue to post this.

    I agree, that you do not need proxyquire at all. I use sinon sandbox, stub and fake in example below.

    // @file stackoverflow.js
    const sinon = require('sinon');
    const { expect } = require('chai');
    const mysql = require('mysql');
    
    // Change this to your mysql class definition.
    const MySQL = require('./mysql.js');
    
    describe('MySQL', function () {
      let sandbox;
    
      before(function () {
        sandbox = sinon.createSandbox();
      });
    
      after(function () {
        sandbox.restore();
      });
    
      it('constructor fn', function () {
        // Prepare stub.
        const stubMysql = sandbox.stub(mysql, 'createConnection');
        // This just to make sure whether conn is storing this true value.
        stubMysql.returns(true);
    
        const test = new MySQL('host', 'user', 'password', 'database');
    
        // Check whether call mysql.createConnection the right way.
        expect(test).to.be.an('object');
        expect(test).to.have.property('conn', true);
        expect(stubMysql.calledOnce).to.equal(true);
        expect(stubMysql.args[0]).to.have.lengthOf(1);
        expect(stubMysql.args[0][0]).to.have.property('host', 'host');
        expect(stubMysql.args[0][0]).to.have.property('user', 'user');
        expect(stubMysql.args[0][0]).to.have.property('password', 'password');
        expect(stubMysql.args[0][0]).to.have.property('database', 'database');
        expect(stubMysql.args[0][0]).to.have.property('port', 3306);
        // Restore stub.
        stubMysql.restore();
      });
    
      it('query fn', async function () {
        let fakeCounter = 0;
        // Create fake function.
        const fakeMysqlQuery = sinon.fake((arg1, arg2, arg3) => {
          // On first response: return fake row.
          if (fakeCounter === 0) {
            fakeCounter += 1;
            arg3(undefined, []);
          }
          // On second response: return error.
          if (fakeCounter > 0) {
            arg3(new Error('TESTQUERY'));
          }
        });
        // Prepare stub.
        const stubMysql = sandbox.stub(mysql, 'createConnection');
        stubMysql.returns({
          query: fakeMysqlQuery,
        });
    
        const test = new MySQL('host', 'user', 'password', 'database');
    
        expect(test).to.be.an('object');
        expect(test).to.have.property('conn');
        expect(test.conn).to.respondTo('query');
        expect(stubMysql.calledOnce).to.equal(true);
        expect(test).to.respondTo('query');
    
        // Test success query.
        const results = await test.query('SELECT * FROM whatever');
    
        expect(results).to.be.an('array');
        expect(results).to.have.lengthOf(0);
        expect(fakeMysqlQuery.calledOnce).to.equal(true);
        expect(fakeMysqlQuery.args[0]).to.have.lengthOf(3);
        expect(fakeMysqlQuery.args[0][0]).to.equal('SELECT * FROM whatever');
        expect(fakeMysqlQuery.args[0][1]).to.be.an('undefined');
        expect(fakeMysqlQuery.args[0][2]).to.be.an('function');
        expect(fakeCounter).to.equal(1);
    
        // Test rejection.
        try {
          await test.query('SELECT * FROM blablabla');
          expect.fail('should not reach here for mysql query test.');
        } catch (error) {
          expect(error).to.have.property('message', 'TESTQUERY');
          expect(fakeMysqlQuery.calledTwice).to.equal(true);
          expect(fakeMysqlQuery.args[1]).to.have.lengthOf(3);
          expect(fakeMysqlQuery.args[1][0]).to.equal('SELECT * FROM blablabla');
          expect(fakeMysqlQuery.args[1][1]).to.be.an('undefined');
          expect(fakeMysqlQuery.args[1][2]).to.be.an('function');
        }
    
        // Restore stub.
        stubMysql.restore();
      });
    
      it('close fn', async function () {
        let fakeCounter = 0;
        // Create fake function.
        const fakeMysqlEnd = sinon.fake((arg1) => {
          // On first response: return fake row.
          if (fakeCounter === 0) {
            fakeCounter += 1;
            arg1();
          }
          // On second response: return error.
          if (fakeCounter > 0) {
            arg1(new Error('TESTCLOSE'));
          }
        });
        // Prepare stub.
        const stubMysql = sandbox.stub(mysql, 'createConnection');
        stubMysql.returns({
          end: fakeMysqlEnd,
        });
    
        const test = new MySQL('host', 'user', 'password', 'database');
    
        expect(test).to.be.an('object');
        expect(test).to.have.property('conn');
        expect(test.conn).to.respondTo('end');
        expect(stubMysql.calledOnce).to.equal(true);
        expect(test).to.respondTo('close');
    
        // Test success close.
        await test.close();
    
        expect(fakeMysqlEnd.calledOnce).to.equal(true);
        expect(fakeMysqlEnd.args[0]).to.have.lengthOf(1);
        expect(fakeMysqlEnd.args[0][0]).to.be.an('function');
        expect(fakeCounter).to.equal(1);
    
        // Test failed close.
        try {
          await test.close();
          expect.fail('should not reach here for mysql end test.');
        } catch (error) {
          expect(error).to.have.property('message', 'TESTCLOSE');
          expect(fakeMysqlEnd.calledTwice).to.equal(true);
          expect(fakeMysqlEnd.args[1]).to.have.lengthOf(1);
          expect(fakeMysqlEnd.args[1][0]).to.be.an('function');
        }
    
        // Restore stub.
        stubMysql.restore();
      });
    });
    
    
    $ npx mocha stackoverflow.js 
    
    
      MySQL
        ✓ constructor fn
        ✓ query fn
        ✓ close fn
    
    
      3 passing (21ms)
    
    $
    

    Hope this helps.