在测试“无限循环”时,最佳做法是什么?

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

我们以此代码为例:

module Blah extend self def run some_initializer_method loop do some_other_method yet_another_method end end end 

我想使用Blah.run测试方法Blah.run (我也使用RR ,但普通的rspec是一个可接受的答案)。

我认为最好的方法是分解更多,比如将循环分成另一种方法或其他方法:

 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_methodyet_another_method但不测试do_some_looping

通过模拟在某个时刻让循环中断?

……别的什么?

更实际的是在单独的线程中执行循环,断言一切正常,然后在不再需要时终止线程。

 thread = Thread.new do Blah.run end assert_equal 0, Blah.foo thread.kill 

如何在一个单独的方法中使用循环体,比如calculateOneWorldIteration ? 这样你就可以根据需要在测试中旋转循环。 它并没有损害API,它是一种非常自然的公共界面方法。

在rspec 3.3中,添加此行

 allow(subject).to receive(:loop).and_yield 

你之前的钩子将简单地屈服于块而没有任何循环

你无法测试那些永远存在的东西。

当面对难以(或不可能)测试的一段代码时,您应该: –

  • 重构以隔离难以测试的代码部分。 使不可测试的部分变得微不足道。 评论以确保它们不会在以后扩展为非平凡的
  • 对其他部件进行unit testing,这些部件现在与难以测试的部分分开
  • 难以测试的部分将由集成或验收测试涵盖

如果游戏中的主循环只进行一次,那么当你运行它时会立即显现出来。

如何模拟循环以便它只执行你指定的次数?

 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 / throw构造来测试无限循环。

引发错误也可能有效,但这并不理想,特别是如果你的循环阻止所有错误,包括exception。 如果您的块没有拯救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 

当我第一次尝试这个时,我担心第二次使用should_receive :some_other_method将“覆盖”第一个,但事实并非如此。 如果您想亲自should_receive ,请向should_receive添加块以查看它是否被称为预期次数:

 blah.should_receive(:some_other_method) { puts 'received some_other_method' } 

:)几个月前我有这个查询 。

简短的回答是没有简单的方法来测试它。 您测试驱动循环的内部。 然后你将它变成一个循环方法并进行手动测试,循环工作直到终止条件发生。

我找到的最简单的解决方案是产生循环一次而不是返回。 我在这里用过摩卡咖啡。

 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 

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

尽管如此,如上所述,最好将循环方法保持为尽可能小和愚蠢。

希望这可以帮助!

我们测试仅在信号上退出的循环的解决方案是将退出条件方法存根,以便第一次返回false,但是第二次返回true,确保循环仅执行一次。

无限循环类:

 class Scheduling::Daemon def run loop do if daemon_received_stop_signal? break end # do stuff end end end 

spec测试循环的行为:

 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