【问题标题】:Mocking es6 class constructor attribute with mocha/sinon用 mocha/sinon 模拟 es6 类构造函数属性
【发布时间】:2020-06-01 00:51:35
【问题描述】:

我有一个小的包装类,它为一些 mysql 功能添加了 Promise。

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

我正在尝试为这个类编写一个单元测试,但我完全陷入了试图模拟 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 属性以便我可以轻松地断言方法调用的最佳方法是什么?任何帮助将不胜感激

【问题讨论】:

    标签: node.js unit-testing mocha.js sinon


    【解决方案1】:

    我迟到了一个小时。 :)

    但我已经对示例进行了编码并提供了替代测试,所以我继续发布。

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

    // @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)
    
    $
    

    希望这会有所帮助。

    【讨论】:

      【解决方案2】:

      您不需要使用proxyquire 模块,该模块主要用于模拟/存根模块中的独立功能。单元测试应该是:

      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方法的测试方法是一样的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-11-01
        • 2021-04-14
        • 2013-01-12
        • 2017-12-18
        • 2018-08-28
        • 2020-08-26
        • 2017-06-29
        • 2020-07-11
        相关资源
        最近更新 更多