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_transition
块 do |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_any
和 MyClassStateMachineObserver.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
此致,
朱尔斯
我有一个用旧的和未维护的 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_transition
块 do |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_any
和 MyClassStateMachineObserver.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
此致,
朱尔斯