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

Phi*_*ien 4 tdd unit-testing mocha.js node.js sinon

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

|--lib
   |--index.js
|--test
   |--test.js
Run Code Online (Sandbox Code Playgroud)

index.js

var myFuncs = {};

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

myFuncs.func2 = function(data) {
};

module.exports = myFuncs;
Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)

不可否认,这是非常人为的,但就目前而言,我想测试任何调用func1原因的func2调用,所以我会使用

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

我也想测试它func1会返回200所以我可以使用

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

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

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

这些有何不同?什么是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);
       });
    };
Run Code Online (Sandbox Code Playgroud)

我知道当谈到数据库时,有人建议为此目的使用测试数据库,但是我的最终目标可能是包含许多表的数据库,并且复制它以进行测试可能会很麻烦.我已经看到了使用sinon模拟数据库的参考,并尝试按照这个答案,但我无法弄清楚什么是最好的方法.

Kir*_*tin 11

你在一篇文章中问了很多不同的问题......我会尽力解决.

  1. 使用两个函数测试myFuncs.

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

var spyFunc1 = sinon.spy(myFuncs.func1);
var spyFunc2 = sinon.spy(myFuncs.func2);
Run Code Online (Sandbox Code Playgroud)

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

2.1.describe('Function 1',...)测试套件看起来对我来说太吝啬了.

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

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

var test = sinon.stub().returns(200);
assert.equal(myFuncs.func1(test), 200);
Run Code Online (Sandbox Code Playgroud)

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

myFuncs.func2 = sinon.spy();
assert.equal(myFuncs.func1(test), 200);
assert(myFuncs.func2.calledOnce);
Run Code Online (Sandbox Code Playgroud)

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

myFuncs.func1 = sinon.stub().returns(200);
assert.equal(myFuncs.func1(test), 200);
Run Code Online (Sandbox Code Playgroud)

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

  1. 模拟数据库查询.模拟数据库一直是个障碍.我可以提供一些建议.

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

3.2.有很好的碎片代码.最好存在几个层次.最低(DAL)应与业务逻辑分开.在这种情况下,您将编写业务类的代码,只需模拟DAL.为了测试DAL你可以有你所提到的方法(sinon.mock整个模块)或一些特定的库(如描述例:使用SQLite数据库更换发动机测试这里)

  1. 结论."一旦我的程序变得更复杂,这些简单的测试方法将如何发展".

除非您在考虑测试的情况下开发应用程序,因此很难维护单元测试.坚持主要规则 - 保持每个单元测试尽可能小.否则你是对的,它最终会变得凌乱.因为应用程序的不断发展的逻辑将涉及您的测试代码.