使用 Bookshelf.js 和 knex.js 进行单元测试

Unit testing with Bookshelf.js and knex.js

我是 Node 的新手,正在使用 knex 和书架开发一个项目。我在对我的代码进行单元测试时遇到了一些麻烦,我不确定自己做错了什么。

基本上我有一个如下所示的模型(称为 VorcuProduct):

var VorcuProduct = bs.Model.extend({
    tableName: 'vorcu_products'
});

module.exports.VorcuProduct = VorcuProduct

还有一个函数,如果 VorcuProduct 在数据库中不存在,则保存它。非常简单。执行此操作的函数如下所示:

function subscribeToUpdates(productInformation, callback) {
  model.VorcuProduct
    .where({product_id: productInformation.product_id, store_id: productInformation.store_id})
    .fetch()
    .then(function(existing_model) {
        if (existing_model == undefined) {
            new model.VorcuProduct(productInformation)
                .save()
                .then(function(new_model) { callback(null, new_model)})
                .catch(callback);
        } else {
            callback(null, existing_model)
        }
    })
}

在不访问数据库的情况下,哪种测试方法是正确的?我是否需要模拟 fetch 到 return 模型或未定义(取决于测试),然后对 save 做同样的事情?我应该为此使用重新布线吗?

如您所见,我有点迷路了,我们将不胜感激。

谢谢!

我一直在使用 in-memory Sqlite3 databases 进行自动化测试,并取得了巨大成功。我的测试 运行 对 MySQL 需要 10 到 15 分钟,但使用内存中的 sqlite3 数据库只需要 30 秒左右。使用 :memory: 作为连接字符串以利用此技术。

关于单元测试的说明 - 这不是真正的单元测试,因为我们仍在运行对数据库进行查询。这是技术上的集成测试,但是它 运行s 在合理的时间段内,如果你有一个查询密集型应用程序(比如我的)那么这种技术将被证明比单元测试更有效地捕获错误。

Gotchas - Knex/Bookshelf 在应用程序启动时初始化连接,这意味着您保持测试之间的上下文。我建议编写一个模式 create/destroy 脚本,以便您为每个测试构建和销毁表。此外,Sqlite3 对外键约束的敏感度低于 MySQL 或 PostgreSQL,因此请确保您 运行 您的应用程序不时针对其中之一,以确保您的约束正常工作。

这实际上是一个很好的问题,它提出了单元测试的价值和局限性。

在这种特殊情况下,非存根逻辑非常简单——只是一个简单的 if 块,所以这是否值得进行单元测试是有争议的,所以接受的答案是一个很好的答案并指出小规模集成测试的价值。

另一方面,进行单元测试的练习仍然很有价值,因为它指出了代码改进的机会。一般来说,如果测试太复杂,底层代码可能会使用一些重构。在这种情况下,可能会重构 doesProductExist 函数。从 knex/bookshelf 返回承诺而不是转换为回调也将是一个有用的简化。

但为了比较,这里是我对现有代码的真正单元测试的看法:

var rewire = require('rewire');
var sinon = require('sinon');
var expect = require('chai').expect;
var Promise = require('bluebird');
var subscribeToUpdatesModule = rewire('./service/subscribe_to_updates_module');

var subscribeToUpdates = subscribeToUpdatesModule.__get__(subscribeToUpdates);

describe('subscribeToUpdates', function () {
  before(function () {
    var self = this;
    this.sandbox = sinon.sandbox.create();
    var VorcuProduct = subscribeToUpdatesModule.__get__('model').VorcuProduct;

    this.saveStub = this.sandbox.stub(VorcuProduct.prototype, 'save');
    this.saveStub.returns(this.saveResultPromise);

    this.fetchStub = this.sandbox.stub()
    this.fetchStub.returns(this.fetchResultPromise);

    this.sandbox.stub(VorcuProduct, 'where', function () {
      return { fetch: self.fetchStub };
    })

  });

  afterEach(function () {
    this.sandbox.restore();
  });

  it('calls save when fetch of existing_model succeeds', function (done) {
    var self = this;
    this.fetchResultPromise = Promise.resolve('valid result');
    this.saveResultPromise = Promise.resolve('save result');
    var callback = function (err, result) {
      expect(err).to.be.null;
      expect(self.saveStub).to.be.called;
      expect(result).to.equal('save result');
      done();
    };
    subscribeToUpdates({}, callback);
  });

  // ... more it(...) blocks

});