2015-01-08 5 views
17

Я относительно новичок в Node и работаю над проектом, используя knex и книжную полку. У меня немного блок проблем, проверяющий мой код, и я не уверен, что я делаю неправильно.Единичное тестирование с помощью Bookshelf.js и knex.js

В принципе у меня есть модель (так называемый 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? Должен ли я использовать rewire для этого?

Как вы можете видеть, я немного потерян, поэтому любая помощь будет оценена.

Спасибо!

ответ

16

Я использую in-memory Sqlite3 databases для автоматического тестирования с большим успехом. Мои тесты занимают от 10 до 15 минут, чтобы работать против MySQL, но всего за 30 секунд или около того в базе данных sqlite3 в памяти. Используйте :memory: для вашей строки подключения, чтобы использовать эту технику.

Примечание об устройстве tesing - Это не истинное модульное тестирование, так как мы все еще выполняем запрос к базе данных. Это технически интеграционное тестирование, однако оно выполняется в течение разумного периода времени, и если у вас есть приложение с большим количеством запросов (например, мое), то этот метод будет более эффективен при обнаружении ошибок, чем модульное тестирование.

Gotchas - Knex/Bookshelf инициализирует соединение в начале приложения, а это означает, что вы сохраняете контекст между тестами. Я бы рекомендовал написать сценарий create/destroy схемы, чтобы вы могли создавать и уничтожать таблицы для каждого теста. Кроме того, Sqlite3 менее чувствителен к ограничениям внешнего ключа, чем MySQL или PostgreSQL, поэтому убедитесь, что вы запускаете приложение против одного из них каждый раз, а затем, чтобы убедиться, что ваши ограничения будут работать должным образом.

+0

Спасибо за ваш обмен опыта. Из любопытства, сколько тестов вы используете? Кроме того, включает ли установка загрузку значительного количества исходных данных? – thebearingedge

+1

@thebearingedge Я запускаю около 70 сценариев с примерно 1000 шагов огурца. Я настраивал и разбивал по 60 таблиц по каждому сценарию. При использовании sqlite-in-памяти для этого требуется менее половины секунды. –

3

Это действительно большой вопрос, который вызывает как ценность, так и ограничения модульного тестирования.

В этом конкретном случае неизображенная логика довольно проста - просто простой блок if, поэтому можно утверждать, стоит ли это затратить на единичное тестирование, поэтому принятый ответ является хорошим и указывает на значение малогабаритного интеграционного тестирования.

С другой стороны, упражнение по тестированию модулей по-прежнему ценно в том, что оно указывает на возможности улучшения кода. В целом, если тесты слишком сложны, базовый код, вероятно, может использовать некоторый рефакторинг. В этом случае функция doesProductExist может быть реорганизована. Возвращение обещаний от knex/bookshelf вместо преобразования в callbacks также было бы полезным упрощением.

Но для сравнения, вот мой взгляд на то, что верно модульное тестирование существующего кода будет выглядеть так:

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 

});