Search code examples
node.jsaws-lambdaamazon-rdssinonmysql2

Sinon mock a function that accepts a callback parameter


I have a Connection class used to create a Mysql connection pool and execute a transaction. The class has method transaction that accepts a callback function as a parameter. The callback passed to the transaction function will be mysql2.query Inserts.

Connection class:

const mysql2 = require('mysql2/promise');

class Connection {
    constructor(options = {}) {
        this.options = options;
    }

    createPool () {
        this.pool = mysql2.createPool({
            host: this.options.host,
            user: this.options.user,
            database:  'my_db',
            ssl: 'Amazon RDS',
            password: this.options.password,
            authPlugins: {
                mysql_clear_password: () => () => Buffer.from(this.options.password + '\0')
            }
        });
    }

    async transaction(callback) {
        const connection = await this.pool.getConnection();
        await connection.beginTransaction();

        try {
            await callback(connection);
            await connection.commit();
        } catch (err) {
            await connection.rollback();
            throw err;
        } finally {
            connection.release();
        }
    }
}
module.exports = { Connection };

Here is how the transaction function is used.

   await conn.transaction(async connection => {
      await connection.query(sql1,[values1]);
      await connection.query(sql2,[values2]);
      await connection.query(sql3,[values3]);
    });

My goal is to mock the async transaction method, but I'm having a difficult time with the callback. Here is what I have tried based on some answers to similar posts and reading the sinon documentation. https://sinonjs.org/releases/v9.2.0/stubs/

it('should test transaction function in Connection', async () => {
    jest.setTimeout(30000);
    const results = { affectedRows: 1 };
    const poolStub = {
        getConnection: sinon.stub().returnsThis(),
        query: sinon.stub().returns(results),
        beginTransaction: sinon.stub().returnsThis(),
        release: sinon.stub(),
    };
    
    const createPoolStub = sinon.stub(mysql2, 'createPool').returns(poolStub);
    const conn = new conns.Connection();
    await conn.createPool();
    const actual = await conn.transaction('select 1 + 1 as solution',[]);

    expect(actual).to.be.eql(1);
    sinon.assert.calledOnce(createPoolStub);
    sinon.assert.calledOnce(poolStub.getConnection);
    sinon.assert.calledWithExactly(poolStub.query, 'select 1 + 1 as solution');
    sinon.assert.calledOnce(poolStub.release);
});

Notice that I am trying to return results = { affectedRows: 1 }; when the query method is called. However this is not working correctly and the query attempt simply times out.

: Timeout - Async callback was not invoked within the 30000ms timeout specified by jest.setTimeout.

As requested, here is the handler code which shows how the Connection class is used, including how it is imported and initialized

const utils = require('./utils');
const conns = require('./connection');

let response = {
  statusCode: 200,
  body: {
    message: 'SQS event processed.',
  },
};

exports.handler = async(event) => {
  try {
    const values1 = [];
    const values2= [];
    const values3 = [];
    for (const currentMessage of event.Records) {
      const data = JSON.parse(currentMessage.body);
      console.log(`Processing Received data`);

      const {record} = data;

      if (record.id == 100) {
        values1.push([record.field1,record.field2, record.field3, Date.now(), Date.now(), 'service-user', 'service-user']);
      }
      if (record.id == 200) {
        values2.push([record.field1,record.field2, record.field3, Date.now(), Date.now(), 'service-user', 'service-user']);
      }
      if (record.id == 300) {
        values3.push([record.field1,record.field2, record.field3, Date.now(), Date.now(), 'service-user', 'service-user']);
      }
    }

    const options = {
      host: 'my-host',
      user: 'service-user'
    };

    const token = utils.getToken(options);
    options.password = token;

    const conn = new conns.Connection(options);
    conn.createPool();

    const sql1 = 'INSERT INTO table1(field1, field2, field4, created_date, modified_date, created_by, modified_by) VALUES ?';
    const sql2 = 'INSERT INTO table2(field1, field2, field4, created_date, modified_date, created_by, modified_by) VALUES ?';
    const sql3 = 'INSERT INTO table3(field1, field2, field4, created_date, modified_date, created_by, modified_by) VALUES ?';

    await conn.transaction(async connection => {
      await connection.query(sql1,[values1]);
      await connection.query(sql2,[values2]);
      await connection.query(sql3,[values3]);
    });


    await conn.pool.end();
    console.log("Connection ended")

  } catch (e) {
    console.log('There was an error while processing', { errorMessage: e});

    response = {
      statusCode: 400,
      body: e
    }
  }

  return response;
};

Solution

  • Here is unit test solution:

    connection.test.js:

    const conns = require('./connection');
    const sinon = require('sinon');
    const mysql2 = require('mysql2/promise');
    
    describe('64255673', () => {
      it('should test transaction function in Connection', async () => {
        const results = { affectedRows: 1 };
        const connectionStub = {
          beginTransaction: sinon.stub(),
          commit: sinon.stub(),
          rollback: sinon.stub(),
          release: sinon.stub(),
        };
        const poolStub = {
          getConnection: sinon.stub().returns(connectionStub),
          query: sinon.stub().returns(results),
        };
        const createPoolStub = sinon.stub(mysql2, 'createPool').returns(poolStub);
    
        const conn = new conns.Connection();
        conn.createPool();
        const callback = sinon.stub();
        await conn.transaction(callback);
    
        sinon.assert.calledOnce(createPoolStub);
        sinon.assert.calledOnce(poolStub.getConnection);
        sinon.assert.calledOnce(connectionStub.beginTransaction);
        sinon.assert.calledOnceWithExactly(callback, connectionStub);
        sinon.assert.calledOnce(connectionStub.commit);
        sinon.assert.calledOnce(connectionStub.release);
      });
    });
    

    unit test result with coverage report:

      64255673
        ✓ should test transaction function in Connection
    
    
      1 passing (13ms)
    
    ---------------|---------|----------|---------|---------|-------------------
    File           | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    ---------------|---------|----------|---------|---------|-------------------
    All files      |   71.43 |      100 |      60 |   76.92 |                   
     connection.js |   71.43 |      100 |      60 |   76.92 | 16,29-30          
    ---------------|---------|----------|---------|---------|-------------------
    

    You can use stub.rejects(value); to make await connection.commit() rejected. Then you can test the code in catch block.