使用 mocha/sinon 模拟 es6 class 构造函数属性

Mocking es6 class constructor attribute with mocha/sinon

我有一个小包装器 class,它为某些 mysql 功能添加了承诺。

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

我正在尝试为此 class 编写单元测试,但完全无法尝试模拟 this.conn

我已经尝试了 proxyquire、sinon 的各种组合,以及两者的结合。当我在 beforeEach 挂钩中使用 proxyquire 时:

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

并尝试为 conn 对象设置一个存根:

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

我不断收到 TypeError: this.conn.query is not a function

将模拟设置为 this.conn 属性的最佳方法是什么,以便我可以轻松断言针对它的方法调用?任何帮助将不胜感激

您不需要使用proxyquire模块,该模块主要用于mock/stub来自模块的独立功能。单元测试应该是:

index.js:

const mysql = require('mysql');

export default class MySQL {
  conn;
  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();
      });
    });
  }
}

index.test.js:

import MySQL from '.';
import sinon from 'sinon';
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);
const expect = chai.expect;
const mysql = require('mysql');

describe('62124221', () => {
  afterEach(() => {
    sinon.restore();
  });
  it('should return query results', async () => {
    const mRows = [1, 2];
    const mConn = {
      query: sinon.stub().callsFake((sql, args, callback) => {
        callback(null, mRows);
      }),
    };
    sinon.stub(mysql, 'createConnection').returns(mConn);
    const db = new MySQL('host', 'user', 'password', 'database');
    const actual = await db.query('select 1;', 'args');
    expect(actual).to.be.deep.equal([1, 2]);
    sinon.assert.calledWithExactly(mysql.createConnection, {
      host: 'host',
      port: 3306,
      user: 'user',
      password: 'password',
      database: 'database',
    });
    sinon.assert.calledWithExactly(mConn.query, 'select 1;', 'args', sinon.match.func);
  });

  it('should return handle error', async () => {
    const mError = new Error('network');
    const mConn = {
      query: sinon.stub().callsFake((sql, args, callback) => {
        callback(mError);
      }),
    };
    sinon.stub(mysql, 'createConnection').returns(mConn);
    const db = new MySQL('host', 'user', 'password', 'database');
    await expect(db.query('select 1;', 'args')).to.be.rejectedWith('network');
    sinon.assert.calledWithExactly(mysql.createConnection, {
      host: 'host',
      port: 3306,
      user: 'user',
      password: 'password',
      database: 'database',
    });
    sinon.assert.calledWithExactly(mConn.query, 'select 1;', 'args', sinon.match.func);
  });
});

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

  62124221
    ✓ should return query results
    ✓ should return handle error


  2 passing (20ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |      60 |       60 |   57.14 |      60 |                   
 index.ts |      60 |       60 |   57.14 |      60 | 29-35             
----------|---------|----------|---------|---------|-------------------

这里只演示如何测试query方法,close方法的测试方法相同

我迟到了一个小时。 :)

但是我已经编写了示例并提供了替代测试,所以我继续 post 这个。

我同意,您根本不需要 proxyquire。我在下面的示例中使用了 sinon 沙箱、存根和伪造。

// @file Whosebug.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 Whosebug.js 


  MySQL
    ✓ constructor fn
    ✓ query fn
    ✓ close fn


  3 passing (21ms)

$

希望对您有所帮助。