Sinon - 何时使用 spies/mocks/stubs 或只是简单的断言?

Sinon - when to use spies/mocks/stubs or just plain assertions?

我想了解如何在节点项目中正确使用 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 是否会 return 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 但我无法弄清楚什么是最好的方法。

你在一个问题上问了很多不同的问题post...我会尽量整理一下。

  1. 正在用两个函数测试 myFuncs。

Sinon 是一个功能广泛的模拟库。 "Mocking" 意味着您应该用模拟或存根替换将要测试的部分内容。 Sinon documentation 中有一篇很好的文章很好地描述了差异。 当您在这种情况下创建间谍时...

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

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

2.1。 describe('Function 1', ...) 测试套件在我看来太做作了。

不清楚你的意思是哪个问题。 return 为常数值的函数不是现实生活中的例子。在大多数情况下,会有一些参数,被测函数将实现一些转换输入参数的算法。因此,在您的测试中,您将部分地实施相同的算法来检查函数是否正常工作。这就是 TDD 的用武之地,它实际上假设您从测试开始实施并采用部分单元测试代码来实施被测试的方法。

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

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

即使您将 return 部分替换为 100,测试也会 运行 成功。 例如,一个有意义的方法是用存根替换 func2 以避免在测试中启动繁重的 calculation/remote 请求(数据库查询、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. 模拟数据库查询。 模拟数据库一直是一个障碍。我可以提供一些建议。

3.1。有很好的碎片化环境。即使对于一个小项目,也最好存在一个开发、阶段和生产完全独立的环境。包括数据库。这意味着您有一种自动创建数据库的方式:脚本或 ORM。 在这种情况下,您将使用 before()/beforeEach() 轻松维护测试引擎中的测试数据库,为您的测试提供一个干净的结构。

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

  1. 结论。 "how these simple testing approaches would evolve once my program gets more complex".

很难维护单元测试,除非您在开发应用程序时考虑到测试,因此非常分散。坚持主要规则——让每个单元测试尽可能小。否则你是对的,它最终会变得混乱。因为你的应用程序不断发展的逻辑会涉及到你的测试代码。