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

24

我对Node相对较新,正在使用knex和bookshelf进行项目开发。我在单元测试代码方面遇到了一些问题,不确定自己做错了什么。

基本上,我有一个名为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返回一个模型或未定义(取决于测试),然后对save也做同样的事情吗?我应该使用重装模块进行这项工作吗?
如你所见我有点困惑,任何帮助都将不胜感激。
谢谢!
2个回答

23

我一直在使用内存中的Sqlite3数据库进行自动化测试,并取得了很大的成功。与MySQL相比,我的测试需要花费10到15分钟的时间来运行,但只需要30秒左右就可以使用内存中的sqlite3数据库。使用:memory:作为连接字符串来利用这种技术。

关于单元测试的说明 - 这不是真正的单元测试,因为我们仍然要对数据库运行查询。虽然这在技术上是集成测试,但如果你的应用程序具有查询密集型(像我的)特点,那么这种技术将比单元测试更有效地捕获漏洞。

可能出现的问题 - Knex / Bookshelf在应用程序启动时初始化连接,这意味着您在测试之间保留上下文。我建议编写模式创建/销毁脚本,以便您可以为每个测试构建和销毁表格。此外,Sqlite3对于外键约束不如MySQL或PostgreSQL敏感,因此请确保偶尔使用其中一个来运行应用程序,以确保您的约束将正常工作。


感谢您分享您的经验。出于好奇,您运行了多少个测试?此外,设置是否涉及加载大量种子数据? - thebearingedge
1
@thebearingedge 我运行了大约70个场景,总共有约1,000个黄瓜步骤。我在每个场景中设置和拆除60个表格。使用sqlite内存数据库,这只需要不到半秒钟的时间。 - Steven Hunt
1
它有一些缺点。例如,sqlite3不支持jsonb数据类型。 - OhadBasan

4

这实际上是一个很好的问题,涉及到单元测试的价值和限制。

在这种情况下,未存根的逻辑非常简单--只是一个简单的 if 块,因此单元测试是否值得努力进行是可以争论的,因此被接受的答案是不错的,并指出了小规模集成测试的价值。

另一方面,进行单元测试的练习仍然有价值,因为它指出了代码改进的机会。通常,如果测试过于复杂,则潜在的代码可能需要进行一些重构。在这种情况下,doesProductExist 函数可能可以重构出来。将 promises 从 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

});

很棒的答案。有时候,对于许多测试来说,DB并不是一个选项。我使用jest.spyOn().mockImplementation()来监视和重新实现Bookshelf模型类的原型方法。这样你就可以解决任何数据,并验证它们是否已被调用以及传递了哪些参数。 - KeitelDOG

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