Search code examples
javascriptnode.jsunit-testingaws-lambdasinon

Sinon test function call with a callback


I have a Connection class with a transaction function which is used to execute mysql transactions. It takes a callback as a parameter which represents any mysql queries to be executed. With the help of stackoverflow users I have created a unit test for the function itself, but I'm having trouble testing the lambda handler which actually uses the function.

Here is the 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();
            console.log("An exception was thrown and the mysql transaction has been rolled back", err);
        } finally {
            connection.release();
        }
    }
}
module.exports = { Connection };

And here is the lambda handler. I've only included the most relevant part

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

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

exports.handler = async(event) => {
  console.log(event.Records);
  try {
    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]);
    });

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

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

  return response;
};

And here I am trying to unit test the handler:

test('Should test handler without mocking', 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);

    // attempt to mock transaction function
    sinon.stub(conn, 'transaction').returns(results);

    const response = await index.handler(mocks.baseMessage, null);
    expect(response.statusCode).toBe(200);
});

The test seems to work up until I attempt to mock the conn.transaction call. It just gives a ReferenceError: conn is not defined . How can I mock the results of transaction so that the handler function will be covered by tests?


Solution

  • Unit test solution:

    handler.js:

    const conns = require('./connection');
    
    let response = {
      statusCode: 200,
      body: {
        message: 'SQS event processed.',
      },
    };
    
    exports.handler = async (event) => {
      console.log(event.Records);
      const options = {};
      const [values1, values2, values3] = [1, 2, 3];
      try {
        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]);
        });
      } catch (e) {
        console.log('There was an error while processing', { errorMessage: e });
    
        response = {
          statusCode: 400,
          body: e,
        };
      }
    
      return response;
    };
    

    handler.test.js:

    const index = require('./handler');
    const conns = require('./connection');
    const { expect } = require('chai');
    const sinon = require('sinon');
    
    const mocks = {
      baseMessage: {
        Records: [],
      },
    };
    
    describe('64310254', () => {
      it('Should test handler without mocking', async () => {
        const poolConnectionStub = {
          query: sinon.stub(),
        };
        const connectionStub = {
          createPool: sinon.stub(),
          transaction: sinon.stub().callsFake(async (callback) => {
            await callback(poolConnectionStub);
          }),
        };
        const ConnectionStub = sinon.stub(conns, 'Connection').returns(connectionStub);
    
        const response = await index.handler(mocks.baseMessage, null);
        expect(response.statusCode).to.be.eq(200);
        sinon.assert.calledOnceWithExactly(ConnectionStub, {});
        sinon.assert.calledOnce(connectionStub.createPool);
        sinon.assert.calledOnceWithExactly(connectionStub.transaction, sinon.match.func);
        sinon.assert.calledThrice(poolConnectionStub.query);
        ConnectionStub.restore();
      });
    
      it('should handle error', async () => {
        const poolConnectionStub = {
          query: sinon.stub(),
        };
        const connectionStub = {
          createPool: sinon.stub(),
          transaction: sinon.stub().rejects(new Error('timeout')),
        };
        const ConnectionStub = sinon.stub(conns, 'Connection').returns(connectionStub);
    
        const response = await index.handler(mocks.baseMessage, null);
        expect(response.statusCode).to.be.eq(400);
        sinon.assert.calledOnceWithExactly(ConnectionStub, {});
        sinon.assert.calledOnce(connectionStub.createPool);
        sinon.assert.calledOnceWithExactly(connectionStub.transaction, sinon.match.func);
        sinon.assert.notCalled(poolConnectionStub.query);
        ConnectionStub.restore();
      });
    });
    

    unit test result with coverage report:

      64310254
    []
        ✓ Should test handler without mocking
    []
    There was an error while processing { errorMessage:
       Error: timeout
           at Context.it (/Users/ldu020/workspace/github.com/mrdulin/expressjs-research/src/stackoverflow/64310254/handler.test.js:40:41)
           at callFn (/Users/ldu020/workspace/github.com/mrdulin/expressjs-research/node_modules/mocha/lib/runnable.js:387:21)
           at Test.Runnable.run (/Users/ldu020/workspace/github.com/mrdulin/expressjs-research/node_modules/mocha/lib/runnable.js:379:7)
           at Runner.runTest (/Users/ldu020/workspace/github.com/mrdulin/expressjs-research/node_modules/mocha/lib/runner.js:535:10)
           at /Users/ldu020/workspace/github.com/mrdulin/expressjs-research/node_modules/mocha/lib/runner.js:653:12
           at next (/Users/ldu020/workspace/github.com/mrdulin/expressjs-research/node_modules/mocha/lib/runner.js:447:14)
           at /Users/ldu020/workspace/github.com/mrdulin/expressjs-research/node_modules/mocha/lib/runner.js:457:7
           at next (/Users/ldu020/workspace/github.com/mrdulin/expressjs-research/node_modules/mocha/lib/runner.js:362:14)
           at Immediate.<anonymous> (/Users/ldu020/workspace/github.com/mrdulin/expressjs-research/node_modules/mocha/lib/runner.js:425:5)
           at runCallback (timers.js:705:18)
           at tryOnImmediate (timers.js:676:5)
           at processImmediate (timers.js:658:5) }
        ✓ should handle error
    
    
      2 passing (42ms)
    
    ---------------|---------|----------|---------|---------|-------------------
    File           | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    ---------------|---------|----------|---------|---------|-------------------
    All files      |   63.64 |        0 |   28.57 |   65.63 |                   
     connection.js |   14.29 |        0 |       0 |   15.38 | 5-32              
     handler.js    |     100 |      100 |     100 |     100 |                   
    ---------------|---------|----------|---------|---------|-------------------