新手问题 ActiveRecord 的回调函数该怎么测试?

small_fish__ · 2013年09月16日 · 最后由 small_fish__ 回复于 2013年09月17日 · 3321 次阅读

今天遇到一个 bug,测试始终没有测试出来。

问题描述:

model

after_commit :initialize_library_materials


  def initialize_library_materials
      return unless (self.previous_changes.include?(:owner_id) 
                   || self.previous_changes.include?(:owner_type) ) 
      self.library_materials.create, :owner => library_owner 
      end
    end
  end

测试代码

it "should initialize a library material for the creator on create" do
    lambda do
        material = create :material, :user => @user
        material.send(:initialize_library_materials)  #手动call 的回调,不知道怎么处理好
    end.should change(LibraryMaterial,:count).by(1)   测试是否创建了一个library material
end

出现的 bug

当我在创建该对象以后,调用:initialize_library_materials 以后并没有按照期望创建一个 LibraryMaterial,但是测试是通过了的。

手动排查原因

在创建一条新的数据后,执行 after_commit 后,其实是(create),previous_changes 的返回始终只有 updated_at 这个字段。所以导致 initialize_library_materials 函数在第一行就返回了。


困惑

  • 我想说为什么 rspec 没有测试出这个问题呢?按照我的理解,应该是可以测试出来的。。求朋友们,分析下,为什么 rspec 可以通过。
  • previous_changes 这个方法按照想象的应该是返回所有变化的字段啊。。毕竟我采用的是
material = Material.new
material.title = 'test'
material.save

的形式。。

你或许可以试试 changes 然后测试代码里面应该不需要手动调用 send(:initialize_library_materials) 了吧!

也许问题就出在测试代码的

material.send(:initialize_library_materials)

debugger 一下吧,还有就是

def initialize_library_materials
  return unless (self.previous_changes.include?(:owner_id) 
                   || self.previous_changes.include?(:owner_type) ) 
  self.library_materials.create, :owner => library_owner 
end

这种写法太费脑筋了,转成if不是更好理解吗?

def initialize_library_materials
  if !self.previous_changes.include?(:owner_id) && !self.previous_changes.include?(:owner_type)
    self.library_materials.create :owner => library_owner 
  end
end

#2 楼 @tumayun debuger 有必要了。。。。我居然忘掉了。。

不太理解的地方一:你在测试中去手动调用 callback 方法,这个做法太奇怪了。既然是 callback 方法,就意味着它是被一个 commit 来 trigger 的,而不是手动 trigger 的。

不太理解的地方二:因为 after_commit 是框架提供的,所以问题最可能出现在你的代码逻辑中,且 after_commit 是发生在 save 之后,可一旦对象被 save, 那么所有的 changes 列表就都被清空了呀。

RSpec 如果 use_transactional_fixtures = true 的话,after_commit 不会被执行。

#4 楼 @lgn21st 如果清空的话,那么为什么测试代码可以通过?? 执行 save 在这里可以简单分为 create 和 update 吧,而且通过实验发现,如果执行的是 create 操作,changes is 会被清空,但是执行的是 update,changes 没有被清空,我对这个结果有点不解,,不知道是否是这样一回事,还是我的测试方法有问题。。

#6 楼 @small_fish__ 模型对象 save 之后,changes 表理应被清空,这是 AR 的设计决定。这也就意味着在 before_save 的时候 changes 是可用的。

你可以先按照 @fredwu 的建议,关闭 use_transactional_fixtures,如果 use_transactional_fixturestrue 的话,after_commit 是不会被触发的。关闭的话会有跑测试用例之前清空数据表的问题,你可以考虑换个方案,用 database_cleaner 并辅以 truncate 策略来清空数表,缺点是这会让你的测试变慢。

#7 楼 @lgn21st 我这里用的是 previous_changes 不是 changes..

#7 楼 @lgn21st 其实我想确定下 这里 api 说的 save 其实只是 update 而已,并不包括 create 的情况。。真的是这样吗?

#9 楼 @small_fish__ 你是对的,应该用 previous_changes。这个可以在 Rails Console 中很容易佐证,对于新创建对象而言,create先 new 然后 save 是一样的。

#10 楼 @lgn21st 是的,所以最终的结论就是:previous_changes just for update ?

你在你的 Rails console 中得出的结论是 previous_changes just for update?

>> user = User.create name: 'Daniel'
   (0.1ms)  begin transaction
  SQL (0.8ms)  INSERT INTO "users" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", Tue, 17 Sep 2013 06:04:26 UTC +00:00], ["name", "Daniel"], ["updated_at", Tue, 17 Sep 2013 06:04:26 UTC +00:00]]
   (8.0ms)  commit transaction
=> #<User id: 2, name: "Daniel", created_at: "2013-09-17 06:04:26", updated_at: "2013-09-17 06:04:26">
>> user.previous_changes
=> {"name"=>[nil, "Daniel"], "created_at"=>[nil, Tue, 17 Sep 2013 06:04:26 UTC +00:00], "updated_at"=>[nil, Tue, 17 Sep 2013 06:04:26 UTC +00:00], "id"=>[nil, 2]}

#12 楼 @lgn21st 是的,我本地是空的,但是我用的是先 new ,然后赋值,最后 save 的方式

请问你的 rails 版本是?

#12 楼 @lgn21st 能帮忙测试下吗?

user = User.new 
user.name = 'Daniel'
user.save

#14 楼 @small_fish__ 结果貌似一样。

>> user = User.new
=> #<User id: nil, name: nil, created_at: nil, updated_at: nil>
>> user.name = 'Daniel'
=> "Daniel"
>> user.save
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "users" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", Tue, 17 Sep 2013 06:16:08 UTC +00:00], ["name", "Daniel"], ["updated_at", Tue, 17 Sep 2013 06:16:08 UTC +00:00]]
   (8.0ms)  commit transaction
=> true
>> user.previous_changes
=> {"name"=>[nil, "Daniel"], "created_at"=>[nil, Tue, 17 Sep 2013 06:16:08 UTC +00:00], "updated_at"=>[nil, Tue, 17 Sep 2013 06:16:08 UTC +00:00], "id"=>[nil, 3]}

#13 楼 @small_fish__ Rails 的版本

>> Rails.version
=> "3.2.13"

#16 楼 @lgn21st 我版本和你一样却结论不一样,为什么呢????

需要 登录 后方可回复, 如果你还没有账号请 注册新账号