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

时间:2011-04-19 14:17:38

标签: ruby-on-rails ruby tdd rspec rr

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

我们以此代码为例:

module Blah
  extend self

  def run
     some_initializer_method
     loop do
        some_other_method
        yet_another_method
     end
  end
end

我想使用Rspec测试方法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_method& yet_another_method但不是do_some_looping

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

......别的什么?

10 个答案:

答案 0 :(得分:15)

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

thread = Thread.new do
  Blah.run
end

assert_equal 0, Blah.foo

thread.kill

答案 1 :(得分:10)

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

答案 2 :(得分:9)

在rspec 3.3中,添加此行

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

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

答案 3 :(得分:3)

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

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

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

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

答案 4 :(得分:2)

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

Module Object
    private
    def loop
        3.times { yield }
    end
end

当然,你只能在你的规格中嘲笑它。

答案 5 :(得分:2)

我知道这有点旧,但你也可以使用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

答案 6 :(得分:1)

我几乎总是使用catch / throw构造来测试无限循环。

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

Catch / throw示例:

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添加块,以查看它是否被称为预期次数:

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

答案 7 :(得分:0)

:)几个月前我有this query

简短的回答是没有简单的方法来测试它。您测试驱动循环的内部。然后你把它变成一个循环方法&amp;做一个循环工作直到终止条件的手动测试。

答案 8 :(得分:0)

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

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

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

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

希望这有帮助!

答案 9 :(得分:0)

我们测试仅在信号上退出的循环的解决方案是将退出条件方法存根,以便第一次返回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