如何在Node.js中模拟MySQL(不使用ORM)?

48

我正在使用felixge的node-mysql客户端和Node.js。我没有使用ORM。

我正在使用Vows进行测试,希望能够模拟我的数据库,可能使用Sinon。由于我实际上没有一个DAL(除了node-mysql),所以我不太确定如何做到这一点。我的模型主要是简单的CRUD操作和许多getter。

有什么想法可以实现这个吗?


你能给我更多细节吗?例如,粘贴一些代码并展示一下你想要实现的伪代码?我很乐意帮忙。 - alessioalex
@alessioalex,说实话我真的不知道该从哪里开始。理想情况下,我想看看别人的模型类及其相关的Mock/测试。 - Josh Smith
有没有人使用 Jest 遇到同样的问题? - Melroy van den Berg
6个回答

44

使用 sinon,您可以将模拟或存根放置在整个模块周围。例如,假设 mysql 模块有一个名为 query 的函数:

var mock;

mock = sinon.mock(require('mysql'))
mock.expects('query').with(queryString, queryParams).yields(null, rows);

queryStringqueryParams是您期望的输入。 rows是您期望的输出。

当您要测试的类现在需要mysql并调用query方法时,它将被sinon拦截和验证。

在您的测试预期部分中,您应该有:

mock.verify()

在你的拆除(teardown)函数中,你应该将mysql恢复为正常功能:

mock.restore()

2
".with() 似乎已经被弃用,推荐使用 .withArgs()。我正在测试 sinon 1.7.2。" - AndreiM
Sinon工具非常好用。谢谢。(不再需要为单元测试访问真实数据库!) :) - kiwicomb123

13

将数据库抽象成一个独立的类,使用 MySQL。然后,你可以将该类的实例传递给你的模型构造函数,而不是使用require()来加载它们。

通过这种设置,你可以在单元测试文件中向你的模型传递一个模拟的数据库实例。

以下是一个小示例:

// db.js
var Db = function() {
   this.driver = require('mysql');
};
Db.prototype.query = function(sql, callback) {
   this.driver... callback (err, results);
}
module.exports = Db;

// someModel.js
var SomeModel = function (params) {
   this.db = params.db
}
SomeModel.prototype.getSomeTable (params) {
   var sql = ....
   this.db.query (sql, function ( err, res ) {...}
}
module.exports = SomeModel;

// in app.js
var db = new (require('./db.js'))();
var someModel = new SomeModel ({db:db});
var otherModel = new OtherModel ({db:db})

// in app.test.js
var db = {
   query: function (sql, callback) { ... callback ({...}) }
}
var someModel = new SomeModel ({db:db});

6

我不是很熟悉node.js,但从传统的编程角度来看,要实现这样的测试,你需要抽象出数据访问方法。你能不能创建一个像这样的DAL类:

var DataContainer = function () {
}

DataContainer.prototype.getAllBooks = function() {
    // call mysql api select methods and return results...
}

现在在测试的情况下,可以在初始化时对getAllBooks类进行补丁操作,例如:
DataContainer.prototype.getAllBooks = function() {
    // Here is where you'd return your mock data in whatever format is expected.
    return [];
}

当测试代码被调用时,getAllBooks将被替换为一个返回模拟数据而不是实际调用mysql的版本。需要注意的是,这只是一个大致的概述,因为我对node.js并不完全熟悉。

5

我最终采用了 @kgilpin 的答案,得到了以下类似的代码,用于在 AWS Lambda 中测试 Mysql:

const sinon = require('sinon');
const LambdaTester = require('lambda-tester');
const myLambdaHandler = require( '../../lambdas/myLambda' ).handler;
const mockMysql = sinon.mock(require('mysql'));
const chai = require('chai');
const expect = chai.expect;

describe('Database Write Requests', function() {

 beforeEach(() => {
   mockMysql.expects('createConnection').returns({
     connect: () => {
       console.log('Succesfully connected');
     },
     query: (query, vars, callback) => {
       callback(null, succesfulDbInsert);
     },
     end: () => {
       console.log('Connection ended');
     }
   });

 });
 after(() => {
   mockMysql.restore();
 });

 describe( 'A call to write to the Database with correct schema', function() {

   it( 'results in a write success', function() {

     return LambdaTester(myLambdaHandler)
       .event(anObject)
       .expectResult((result) => {
         expect(result).to.equal(succesfulDbInsert);
       });
   });
 });


 describe( 'database errors', function() {

   before(() => {
     mockMysql.expects('createConnection').returns({
       connect: () => {
         console.log('Succesfully connected');
       },
       query: (query, vars, callback) => {
         callback('Database error!', null);
       },
       end: () => {
         console.log('Connection ended');
       }
     });
   });

   after(() => {
     mockMysql.restore();
   });

   it( 'results in a callback error response', function() {


     return LambdaTester(myLambdaHandler)
       .event(anObject)
       .expectError((err) => {
         expect(err.message).to.equal('Something went wrong');
       });
   });
 });
});

我不需要真正的数据库连接,所以我手动模拟了所有 mysql 的响应。
通过向 .returns 添加另一个函数,您可以模拟任何从 createConnection 调用的方法。


2
干得好 - 不过 - 你可以再进一步:你很好地模拟了静态createConnection api,但是你返回了一个手动创建的连接实例,并失去了sinon的力量。我会保存对原始createConnection的引用,将其存根化,使用stub#callsFake进行编程,在伪造中我会生成一个连接并像kgilpin的示例中那样对其进行存根化。 - Radagast the Brown
嗯,看来还不够清楚。我刚刚说服自己写一篇完整的答案… - Radagast the Brown

2
您可以使用horaa来模拟外部依赖项。
我还相信felixge的node sandboxed-module也可以做类似的事情。
因此,使用kgilpin相同的上下文,在horaa中看起来会像这样:
var mock = horaa('mysql');
mock.hijack('query', function(queryString, queryParam) {
    // do your fake db query (e.g., return fake expected data)
});

//SUT calls and asserts

mock.restore('query');

1
sinon.js 似乎是当今的事实标准 - dule

1

使用mysql驱动程序需要您首先创建一个连接,然后使用返回的连接控制器的API - 您需要采用两步方法。

有两种方法可以做到这一点。

存根createConnection,并使其返回存根连接

在设置期间:

const sinon = require('sinon');
const mysql = require('mysql');
const {createConnection} = mysql;
let mockConnection;
sinon.stub(mysql, 'createConnection').callsFake((...args) => {
    mockConnection = sinon.stub(createConnection.apply(mysql, args))
      .expects('query').withArgs(.... )//program it how you like :)
    return mockConnection;
})

const mockConnectionFactory = 
  sinon.stub(mysql)
  .expects('createConnection')

拆解过程中:
mysql.createConnection.restore();

请注意,这里对实例上的query方法进行了模拟,并且对底层机制没有影响,因此只需还原createConnection

在连接原型上存根化.query方法

这种技术有点棘手,因为mysql驱动程序并没有正式公开其连接以供导入。(好吧,你可以只导入实现连接的模块,但不能保证任何重构不会将其从那里移走)。所以为了获得对原型的引用-我通常创建一个连接并遍历构造函数-原型链:

我通常一行完成,但我会分解步骤并在这里解释:

设置期间:

const realConnection = mysql.createConnection({})
const mockTarget = realConnection.constructor.prototype;
//Then - brutally
consdt mock = sinon.mock(mockTarget).expect('query'....
//OR - as I prefer the surgical manner
sinon.stub(mockTarget, 'query').expect('query'....

拆卸期间
//brutal
mock.restore()
// - OR - surgical:
mockTarget.query.restore()

请注意,我们不会模拟createConnection方法。所有连接参数验证仍将发生(我希望它们发生)。我渴望使用最真实的部分 - 因此只模拟绝对最少量以获得快速测试。但是 - query在原型上被模拟,并且必须被恢复。
还要注意,如果您进行手术,verify将在模拟方法上,而不是在mockTarget上。
这是一个很好的资源:http://devdocs.io/sinon~6-stubs/

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接