在分析由Mongoid事件生成的ActiveRecord对象时,Rspec测试随机失败

我实现了一个基于Mongoid的活动日志记录机制,可以在MongoDB中保存事件。

Mongoid模型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时失败。只有当从命令行运行时才会发生这种情况。当运行此测试时,或者在使用Guard运行所有测试时,它永远不会发生。

在典型的Mongodb设置中,数据库写入成功返回与可以读取数据之间可能存在延迟。 有两个原因:

  • 为了提高性能,在将数据提交到磁盘之前,可以返回“不安全”写入。
  • Mongodb使用副本集,并且存在复制延迟。 通常读取作为负载平衡的forms分发到副本,因此即使您使用安全写入,您可能正在从您刚刚写入的服务器读取不同的服务器,因此看不到您刚刚写入的数据。

为了确保您可以随时立即回读使用Mongoid编写的数据,您需要设置数据库会话选项的consistency: :strong, safe: true ,这两者都不是默认值。

假设问题在于测试设置中的某些竞争条件(而不是代码中),我建议使用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 

[编辑]为了使整个测试更清洁(这不会影响行为,但我不认为)你也可以使用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 

您的测试涵盖了很多unit testing:

  1. 调用after_create回调
  2. 调用after_user_did_something
  3. MyItem对象已创建。

我建议你把它分解成几个unit testing,每个unit testing测试件事。 你从中得到的附加值是,至少,你会知道测试的哪一部分实际上失败了……

 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