Rspec-тесты дают сбой случайно при анализе объектов ActiveRecord, сгенерированных событиями Mongoid
Я реализовал механизм регистрации активности на основе Mongoid, который сохраняет события в MongoDB.
Монгоидная модель Activity
имеет after_create
события, которые выполняют разные задачи в зависимости от типа регистрируемой активности: (упрощенный пример)
class Activity
include Mongoid::Document
after_create do |activity|
method_name = "after_#{activity.event_type}"
send(method_name) if respond_to? method_name
end
def after_user_did_something
MyItem.create!(:type => :user_did_something)
end
end
Тест выглядит так:
it 'should hide previous [objects] create a new updated one' do
2.times do
user.log_activity(:user_did_something)
end
items = MyItems.where(:type => :user_did_something)
items.count.should == 2
end
end
Иногда тесты не проходят на items.count
0 вместо 2. Это происходит только при запуске из командной строки rspec spec
это никогда не происходит при запуске только этого теста или при запуске всех тестов с помощью Guard.
3 ответа
В типичной установке Mongodb может быть задержка между успешным завершением записи в базу данных и тем, когда эти данные могут быть прочитаны. Для этого есть две причины:
- Для повышения производительности "небезопасная" запись может вернуться до того, как данные будут записаны на диск.
- Mongodb использует наборы реплик, и существует задержка репликации. Обычно операции чтения распределяются по репликам как форма балансировки нагрузки, поэтому даже если вы используете безопасную запись, вы можете читать с другого сервера, чем тот, на который вы только что записали, и, следовательно, не видеть только что записанные данные.
Чтобы вы всегда могли сразу же прочитать данные, которые вы только что написали, используя Mongoid, вам нужно установить параметры сеанса базы данных. consistency: :strong, safe: true
, ни один из которых не является значением по умолчанию.
Ваш тест охватывает многое для юнит-теста:
- Тот
after_create
обратный вызов называется - Тот
after_user_did_something
называется - Тот
MyItem
объект был создан.
Я предлагаю вам разбить его на несколько юнит-тестов, каждый из которых тестирует одно. Дополнительная выгода, которую вы получаете от этого, состоит в том, что, по крайней мере, вы будете знать, какая часть теста действительно провалилась...
class Activity
include Mongoid::Document
after_create { |activity| my_after_create_callback(activity.event_type) }
def my_after_create_callback(activity_type)
method_name = "after_#{activity_type}"
send(method_name) if respond_to? method_name
end
def after_user_did_something
MyItem.create!(:type => :user_did_something)
end
end
it 'should call after_create' do
expect_any_instance_of(Activity).to receive(:my_after_create_callback)
.with(:user_did_something)
user.log_activity(:type => :user_did_something)
end
it 'should call the correct after activity method' do
expect_any_instance_of(Activity).to receive :after_user_did_something
user.log_activity(:type => :user_did_something)
end
it 'should create new MyItem' do
expect(MyItem).to receive(:create!).with(:type => :user_did_something)
Activity.new.after_user_did_something
end
Предполагая, что проблема заключается в некотором состоянии гонки в вашей настройке тестирования (а не в вашем коде), я бы рекомендовал использовать ожидания rspec, которые должны подождать, пока объекты будут созданы в БД, прежде чем их считать:
it 'should hide previous [objects] create a new updated one' do
items = MyItems.where(:type => :user_did_something)
expect { 2.times { user.log_activity(:user_did_something) } }.
to change { items.count }.from(0).to(2)
end
[edit] Чтобы сделать тест немного чище в целом (хотя это не повлияет на поведение, я не думаю), вы также можете использовать ленивую загрузку rspec let
, как это:
let(:items_count) { MyItems.where(:type => :user_did_something).count }
it 'should hide previous [objects] create a new updated one' do
expect { 2.times { user.log_activity(:user_did_something) } }.
to change { items_count }.from(0).to(2)
end