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, ни один из которых не является значением по умолчанию.

Ваш тест охватывает многое для юнит-теста:

  1. Тот after_create обратный вызов называется
  2. Тот after_user_did_something называется
  3. Тот 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
Другие вопросы по тегам