26

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

让我们以这段代码为例:

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

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

……别的?

4

10 回答 10

15

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

thread = Thread.new do
  Blah.run
end

assert_equal 0, Blah.foo

thread.kill
于 2011-04-19T14:25:41.697 回答
12

在 rspec 3.3 中,添加此行

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

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

于 2015-10-26T14:40:22.553 回答
10

将循环体放在一个单独的方法中怎么样,比如calculateOneWorldIteration?这样,您可以根据需要在测试中旋转循环。而且它不会损害 API,它是一种在公共接口中非常自然的方法。

于 2011-04-19T14:25:48.583 回答
3

您无法测试永远运行的东西。

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

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

如果你的游戏中的主循环只运行一次,当你运行它时,这将立即显而易见。

于 2012-04-09T00:47:50.710 回答
2

模拟循环以使其仅执行您指定的次数怎么样?

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

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

于 2011-04-19T14:46:02.023 回答
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
于 2012-04-09T00:31:32.507 回答
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

接/扔示例:

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' }
于 2012-09-13T16:04:36.740 回答
1

我们测试仅在信号上退出的循环的解决方案是将退出条件方法存根,第一次返回 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
于 2014-03-05T02:39:54.607 回答
0

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

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

于 2012-04-09T12:41:57.343 回答
0

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

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

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

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

希望这可以帮助!

于 2012-12-22T14:07:34.193 回答