Ruby on rails - 测试"infinite loops"时的最佳实践是什么?



我的基本逻辑是让一个无限循环在某个地方运行,并尽可能地测试它。拥有无限循环的原因并不重要(游戏的主循环、类似守护进程的逻辑……(,我更多的是询问关于这种情况的最佳实践。

让我们以这个代码为例:

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

我们期望循环实际执行,并确保它在一次迭代后退出。

尽管如此,如上所述,保持循环方法尽可能小而愚蠢是一种很好的做法。

希望这能有所帮助!

相关内容

  • 没有找到相关文章

最新更新