使用 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)
$
希望对您有所帮助。
我有一个小包装器 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)
$
希望对您有所帮助。