Sinon - 何时使用间谍/模拟/存根或只是简单的断言?

时间:2015-02-20 10:20:44

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

我试图了解如何在节点项目中正确使用Sinon。我已经通过了示例和文档,但我仍然没有得到它。我已经设置了一个具有以下结构的目录,以尝试使用各种Sinon功能并了解它们适合的位置

|--lib
   |--index.js
|--test
   |--test.js

index.js

var myFuncs = {};

myFuncs.func1 = function () {
   myFuncs.func2();
   return 200;
};

myFuncs.func2 = function(data) {
};

module.exports = myFuncs;

test.js从以下

开始
var assert = require('assert');
var sinon = require('sinon');
var myFuncs = require('../lib/index.js');

var spyFunc1 = sinon.spy(myFuncs.func1);
var spyFunc2 = sinon.spy(myFuncs.func2);

不可否认,这是非常人为的,但就目前情况而言,我想测试对func1的任何调用都会导致调用func2,因此我会使用

describe('Function 2', function(){
   it('should be called by Function 1', function(){
      myFuncs.func1();
      assert(spyFunc2.calledOnce);
   });
});

我还想测试一下func1会返回200所以我可以使用

describe('Function 1', function(){
   it('should return 200', function(){
      assert.equal(myFuncs.func1(), 200);
   });
});

但我也看到了在这种情况下使用stubs的例子,例如

describe('Function 1', function(){
   it('should return 200', function(){
      var test = sinon.stub().returns(200);
      assert.equal(myFuncs.func1(test), 200);
   });
});

这些有何不同? stub给出了一个简单的断言测试不是什么?

我最难以理解的是,一旦我的程序变得更复杂,这些简单的测试方法将如何发展。假设我开始使用mysql并添加新功能

myFuncs.func3 = function(data, callback) {
   connection.query('SELECT name FROM users WHERE name IN (?)', [data], function(err, rows) {
          if (err) throw err;
          names = _.pluck(rows, 'name');
          return callback(null, names);
       });
    };

我知道在涉及数据库的时候,有些人建议为此目的使用测试数据库,但我的最终目标可能是包含许多表的数据库,并且复制它以进行测试可能会很麻烦。我已经看到了使用sinon模拟数据库的参考,并尝试了this answer,但我无法弄清楚什么是最好的方法。

1 个答案:

答案 0 :(得分:11)

你在一篇帖子上问了很多不同的问题......我会尽力解决。

  1. 使用两个函数测试myFuncs。
  2. Sinon是一个具有广泛功能的模拟库。 "嘲讽"意味着你应该用模拟或存根替换将要测试的部分内容。 Sinon documentation中有一篇很好的文章很好地描述了这种差异。 在这种情况下你创建了一个间谍......

    var spyFunc1 = sinon.spy(myFuncs.func1);
    var spyFunc2 = sinon.spy(myFuncs.func2);
    

    ...你刚刚创建了一个观察者。 myFuncs.func1和myFuncs.func2将替换为spy-function,但它将用于记录调用参数并在此之后调用实际函数。这是一种可能的情况,但请注意myFuncs.func1 / func2的所有可能复杂的逻辑将在测试中被调用后运行(例如:数据库查询)。

    2.1。描述('功能1',...)测试套件对我来说看起来太狡猾了。

    你实际上指的是哪个问题并不明显。 返回常量值的函数不是现实生活中的例子。在大多数情况下会有一些参数,被测函数会实现一些转换输入参数的算法。因此,在您的测试中,您将部分实施相同的算法以检查该功能是否正常工作。这就是TDD到位的地方,它实际上假设你从测试开始实现并采用部分单元测试代码来实现被测试的方法。

    2.2。存根。单元测试的第二个版本在给定的示例中看起来没用。 func1不接受任何参数。

    var test = sinon.stub().returns(200);
    assert.equal(myFuncs.func1(test), 200);
    

    即使用100替换返回部分,测试也会成功运行。 有意义的是,例如,用存根替换func2以避免在测试中启动繁重的计算/远程请求(数据库查询,http或其他API请求)。

    myFuncs.func2 = sinon.spy();
    assert.equal(myFuncs.func1(test), 200);
    assert(myFuncs.func2.calledOnce);
    

    单元测试的基本规则是单元测试应尽可能简单,检查代码的最小可能片段。在这个测试中,func1正在测试中,所以我们可以忽略func2的逻辑。哪个应该在另一个单元测试中测试。 请注意,进行以下尝试是没用的:

    myFuncs.func1 = sinon.stub().returns(200);
    assert.equal(myFuncs.func1(test), 200);
    

    因为在这种情况下你用一个存根屏蔽了真正的func1逻辑,你实际上正在测试sinon.stub()。return()。相信我,它运作良好! :d

    1. 模拟数据库查询。 模拟数据库一直是个障碍。我可以提供一些建议。
    2. 3.1。拥有良好的零散环境。即使对于小型项目,也更好地存在开发,阶段和生产完全独立的环境。包括数据库。这意味着您可以自动创建数据库:脚本或ORM。 在这种情况下,您可以使用before()/ beforeEach()轻松地在测试引擎中维护测试数据库,以便为测试提供干净的结构。

      3.2。有很好的碎片代码。最好存在几个层次。最低(DAL)应与业务逻辑分开。在这种情况下,您将编写业务类的代码,只需模拟DAL。要测试DAL,你可以使用你提到的方法(sinon.mock整个模块)或一些特定的库(例如:用SQLite替换db引擎进行测试,如here所述)

      1. 结论。 "一旦我的程序变得更加复杂,这些简单的测试方法将会如何变化"
      2. 除非您在考虑测试的情况下开发应用程序,因此很难维护单元测试,因此,碎片化程度很高。坚持主要规则 - 保持每个单元测试尽可能小。否则你是对的,它最终会变得混乱。因为应用程序的不断发展的逻辑将涉及您的测试代码。