Rspec 模拟 state_machine 回调

Rspec mock state_machine callbacks

我有一个用旧的和未维护的 state_machine gem (https://github.com/pluginaweek/state_machine) 编码的状态机。

就像在示例中一样,我有转换回调。
例如:

# my_class.rb
state_machine :state_machine_name, :initial => :initial_state do

   event :go_to_toto do
     transition :initial_state => :toto
   end

   event :from_toto_to_tata do
     transition :toto => :tata
   end

   ... 

   after_transition :on => any do |person|
     # dumb code for example purpose only
     log.info(person.name) 
     say_hello(person)
     say_goodbye(person)
     ... 
   end

   after_transition :on => :go_to_toto do |person, transition|
      # again, dumb code for example purpose only
      send_mail(person) 
      call(person)
      ...
   end

end

现在,我想在我的状态机上添加一些测试,但我需要模拟 after_transition 调用。

我在某处找到了第一个解决方案,但我不喜欢它,因为围绕转换发生的事情在这个解决方案中可读性较差。

我没有将代码行放在 after_transitiondo |object| ... end 中,而是将这行代码放在一个对象的方法中(可能有点滥用,称为 "observer") 并在 after_transition 块中仅调用此方法:

# my_class.rb
state_machine :state_machine_name, :initial => :initial_state do

   event :go_to_toto do
     transition :initial_state => :toto
   end

   event :from_toto_to_tata do
     transition :toto => :tata
   end

   ... 

   after_transition :on => any do |person|
     MyClassStateMachineObserver.on_any(person)
   end

   after_transition :on => :go_to_toto do |person, transition|
     MyClassStateMachineObserver.go_to_toto(person)
   end
end

# my_class_state_machine_observer.rb
class MyClassStateMachineObserver

  def self.on_any(person)
    # dumb code for example purpose only
    log.info(person.name) 
    say_hello(person)
    say_goodbye(person)
    ... 
  end 

  def self.go_to_toto(person)
    # again, dumb code for example purpose only
    send_mail(person) 
    call(person)
    ...
  end
end

然后,我只需要模拟 MyClassStateMachineObserver.on_anyMyClassStateMachineObserver.go_to_toto 方法的调用,这对于 Rspec.

来说很容易做到

使用第一个解决方案,我的所有测试都是绿色的,但我的代码可读性较差。

经过大量研究和调试会话后,我可能找到了一个无需修改状态机代码的解决方案:

# my_class_spec.rb
let!(:mocks) {
    MyClass.state_machines[:state_machine_name].callbacks.flat_map{ |k, callbackArray| callbackArray }.map{ |callback|
      allow(callback.branch).to receive(:if_condition).and_return(lambda {false})
    }
 }

解决方案来自阅读文档和阅读 state_machine gem 的测试。

state_machine 回调对象有一个 Branch 对象作为只读实例变量。 (https://github.com/pluginaweek/state_machine/blob/master/lib/state_machine/callback.rb#L107)

Branch 对象有一个 if_condition 作为只读实例变量。 (https://github.com/pluginaweek/state_machine/blob/master/lib/state_machine/branch.rb#L15)

如果调用if_condition的结果为false,回调好像没有执行。 (https://github.com/pluginaweek/state_machine/blob/master/test/unit/callback_test.rb#L290)

第二个解决方案似乎正确地模拟了我的回调,但它似乎模拟了很多东西,因为我的测试现在是红色的。
这些州不再播放:/

有人知道模拟此回调的好的解决方案吗?

我在任何地方都找不到关于这个主题的好的回应。

朱尔斯

好的,我终于找到了解决方案。

这是解决方案:

MyClass
  .state_machines[:state_machine_name]
  .callbacks
  .flat_map{ |k, callbackArray| callbackArray }
  .find_all{ |callback|
    callback.instance_variable_get('@methods').any? { |callback_method_proc|
      /my_class/.match callback_method_proc.to_s
    }
  }.map{ |our_callback|
    allow(our_callback.branch).to receive(:if_condition).and_return(lambda {false})
  }

下面是关于这段代码的一些解释:

首先,我得到了回调。这是一个 Map,其中键是回调类型(:before, :after, :around, :failure),值是回调数组:

MyClass
  .state_machines[:state_machine_name]
  .callbacks

然后我将这张地图展平以获得所有回调的数组。如果我想过滤所有回调类型,我不需要保留回调类型:

MyClass
  .state_machines[:state_machine_name]
  .callbacks
  .flat_map{ |k, callbackArray| callbackArray }

然后(这里有点棘手),我过滤回调以仅保留我在状态机中声明的回调。

state_machine gem 在您的状态机中添加了一些回调来完成它的工作。
因此,实际上,在我的第一个 post 的第二个解决方案中,我模拟了太多的回调(state_machine gem 回调和我的回调)。

为了辨别哪个Callback是我的,经过一番研究,我发现一个Callback是由一个@methods包含Procs的私有实例变量组成的。
每个 Proc 引用都包含包含其代码的文件的名称。
所以,我只保留回调,其中 @method Procs 引用包含包含我的状态机代码的文件的名称(丑陋的把戏,我知道;)):

MyClass
  .state_machines[:state_machine_name]
  .callbacks
  .flat_map{ |k, callbackArray| callbackArray }
  .find_all{ |callback|
    callback.instance_variable_get('@methods').any? { |callback_method_proc|
      /my_class/.match callback_method_proc.to_s
    }
  }

最后,我禁止回调调用:

MyClass
  .state_machines[:state_machine_name]
  .callbacks
  .flat_map{ |k, callbackArray| callbackArray }
  .find_all{ |callback|
    callback.instance_variable_get('@methods').any? { |callback_method_proc|
      /my_class/.match callback_method_proc.to_s
    }
  }.map{ |our_callback|
    allow(our_callback.branch).to receive(:if_condition).and_return(lambda {false})
  }

我写了一个更通用的助手 class 可以模拟任何状态机回调和任何状态机回调类型(:之前,:之后,:周围,:失败):

https://gist.github.com/guizmaii/d8571351557ac1e94561

此致,
朱尔斯