我的基本逻辑是让一个无限循环在某个地方运行,并尽可能地测试它。拥有无限循环的原因并不重要(游戏的主循环、类似守护进程的逻辑……(,我更多的是询问关于这种情况的最佳实践。
让我们以这个代码为例:
module Blah
extend self
def run
some_initializer_method
loop do
some_other_method
yet_another_method
end
end
end
我想使用Rspec测试方法Blah.run
(我也使用RR,但普通的rsspec是可以接受的答案(。
我认为最好的方法是分解更多,比如将循环分离成另一种方法或其他东西:
module Blah
extend self
def run
some_initializer_method
do_some_looping
end
def do_some_looping
loop do
some_other_method
yet_another_method
end
end
end
这使我们能够测试run
并模拟循环。。。但在某个时刻,需要对循环中的代码进行测试。
那么在这种情况下你会怎么做呢?
根本不测试这个逻辑,意味着测试some_other_method
&yet_another_method
而不是do_some_looping
?
循环是否通过模拟在某个时刻中断?
还有别的吗?
更实际的做法是在单独的线程中执行循环,断言一切正常工作,然后在不再需要时终止线程。
thread = Thread.new do
Blah.run
end
assert_equal 0, Blah.foo
thread.kill
在rspec 3.3中,添加此行
allow(subject).to receive(:loop).and_yield
到您的beforehook将简单地屈服于块,而不需要任何循环
把循环的主体放在一个单独的方法中怎么样,比如calculateOneWorldIteration
?这样,您就可以根据需要在测试中旋转循环。而且它不会损害API,这是一个很自然的方法,可以在公共接口中使用。
你不能测试那些永远运行的东西。
当面对一段很难(或不可能(测试的代码时,你应该:-
- 重构以隔离代码中难以测试的部分。把不稳定的部分变得微小而琐碎。评论以确保它们不会在以后扩展为无关紧要的内容
- 单元测试其他部分,这些部分现在与难以测试的部分分离
- 难以测试的部分将包含在集成或验收测试中
如果游戏中的主循环只循环一次,那么当你运行它时,这一点会立即显现出来
模拟循环,使其只执行指定的次数,怎么样?
Module Object
private
def loop
3.times { yield }
end
end
当然,你只是在嘲笑你的眼镜。
我知道这有点过时,但您也可以使用yield方法来伪造块,并将单个迭代传递给循环方法。这应该允许您在循环中测试正在调用的方法,而无需将其实际放入无限循环中。
require 'test/unit'
require 'mocha'
class Something
def test_method
puts "test_method"
loop do
puts String.new("frederick")
end
end
end
class LoopTest < Test::Unit::TestCase
def test_loop_yields
something = Something.new
something.expects(:loop).yields.with() do
String.expects(:new).returns("samantha")
end
something.test_method
end
end
# Started
# test_method
# samantha
# .
# Finished in 0.005 seconds.
#
# 1 tests, 2 assertions, 0 failures, 0 errors
我几乎总是使用catch/sthrow构造来测试无限循环。
引发错误也可能有效,但这并不理想,尤其是如果循环的块挽救了所有错误,包括异常。如果您的块没有拯救Exception(或其他错误类(,那么您可以将Exception(或另一个未拯救类(划分为子类,并拯救您的子类:
异常示例
设置
class RspecLoopStop < Exception; end
测试
blah.stub!(:some_initializer_method)
blah.should_receive(:some_other_method)
blah.should_receive(:yet_another_method)
# make sure it repeats
blah.should_receive(:some_other_method).and_raise RspecLoopStop
begin
blah.run
rescue RspecLoopStop
# all done
end
接球/投掷示例:
blah.stub!(:some_initializer_method)
blah.should_receive(:some_other_method)
blah.should_receive(:yet_another_method)
blah.should_receive(:some_other_method).and_throw :rspec_loop_stop
catch :rspec_loop_stop
blah.run
end
当我第一次尝试时,我担心在:some_other_method
上第二次使用should_receive
会"覆盖"第一次,但事实并非如此。如果你想亲眼看看,可以在should_receive
中添加块,看看它是否被调用了预期的次数:
blah.should_receive(:some_other_method) { puts 'received some_other_method' }
我们测试只在信号上退出的循环的解决方案是将退出条件方法截断,第一次返回false,第二次返回true,确保循环只执行一次。
具有无限循环的类:
class Scheduling::Daemon
def run
loop do
if daemon_received_stop_signal?
break
end
# do stuff
end
end
end
测试环路性能的规范:
describe Scheduling::Daemon do
describe "#run" do
before do
Scheduling::Daemon.should_receive(:daemon_received_stop_signal?).
and_return(false, true) # execute loop once then exit
end
it "does stuff" do
Scheduling::Daemon.run
# assert stuff was done
end
end
end
:(几个月前我收到了这个查询。
简单的答案是没有简单的方法来检验这一点。您对循环的内部进行了测试驱动。然后你把它放入一个循环方法&手动测试循环是否工作,直到出现终止条件。
我找到的最简单的解决方案是一次生成循环,然后返回。我在这里用过摩卡咖啡。
require 'spec_helper'
require 'blah'
describe Blah do
it 'loops' do
Blah.stubs(:some_initializer_method)
Blah.stubs(:some_other_method)
Blah.stubs(:yet_another_method)
Blah.expects(:loop).yields().then().returns()
Blah.run
end
end
我们期望循环实际执行,并确保它在一次迭代后退出。
尽管如此,如上所述,保持循环方法尽可能小而愚蠢是一种很好的做法。
希望这能有所帮助!