带有回调的 Sinon 测试函数调用

Sinon test function call with a callback

我有一个连接 class,带有用于执行 mysql 交易的交易功能。它采用回调作为参数,表示要执行的任何 mysql 查询。在 Whosebug 用户的帮助下,我为函数本身创建了一个单元测试,但我在测试实际使用该函数的 lambda 处理程序时遇到了问题。

这是连接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 + '[=10=]')
            }
        });
    }

    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 };

这是 lambda 处理程序。我只包含了最相关的部分

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;
};

我在这里尝试对处理程序进行单元测试:

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);
});

在我尝试模拟 conn.transaction 调用之前,测试似乎一直有效。它只是给出了一个ReferenceError: conn is not defined 。我怎样才能模拟 transaction 的结果,以便处理函数将被测试覆盖?

单元测试解决方案:

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();
  });
});

带有覆盖率报告的单元测试结果:

  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/Whosebug/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 |                   
---------------|---------|----------|---------|---------|-------------------