测试“无限循环”时的最佳实践是什么?

发布于 2024-11-02 10:18:38 字数 948 浏览 5 评论 0 原文

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

让我们以这段代码为例:

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

循环是否会通过模拟在某个时刻中断?

……还有别的事吗?

My basic logic is to have an infinite loop running somewhere and test it as best as possible. The reason for having an infinite loop is not important (main loop for games, daemon-like logic...) and I'm more asking about best practices regarding a situation like that.

Let's take this code for example:

module Blah
  extend self

  def run
     some_initializer_method
     loop do
        some_other_method
        yet_another_method
     end
  end
end

I want to test the method Blah.run using Rspec (also I use RR, but plain rspec would be an acceptable answer).

I figure the best way to do it would be to decompose a bit more, like separating the loop into another method or something:

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

... this allows us to test run and mock the loop... but at some point the code inside the loop needs to be tested.

So what would you do in such a situation?

Simply not testing this logic, meaning test some_other_method & yet_another_method but not do_some_looping ?

Have the loop break at some point via a mock?

... something else?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

错爱 2024-11-09 10:18:39

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

thread = Thread.new do
  Blah.run
end

assert_equal 0, Blah.foo

thread.kill

What might be more practical is to execute the loop in a separate thread, assert that everything is working correctly, and then terminate the thread when it is no longer required.

thread = Thread.new do
  Blah.run
end

assert_equal 0, Blah.foo

thread.kill
节枝 2024-11-09 10:18:39

在 rspec 3.3 中,将此行添加

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

到您的 before 钩子中将简单地屈服于块而无需任何循环

in rspec 3.3, add this line

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

to your before hook will simple yield to the block without any looping

余生一个溪 2024-11-09 10:18:39

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

How about having the body of the loop in a separate method, like calculateOneWorldIteration? That way you can spin the loop in the test as needed. And it doesn’t hurt the API, it’s quite a natural method to have in the public interface.

苏别ゝ 2024-11-09 10:18:39

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

当遇到难以(或不可能)测试的代码部分时,您应该: -

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

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

You can not test that something that runs forever.

When faced with a section of code that is difficult (or impossible) to test you should:-

  • Refactor to isolate the difficult to test part of the code. Make the untestable parts tiny and trivial. Comment to ensure they are not later expanded to become non-trivial
  • Unit test the other parts, which are now separated from the difficult to test section
  • The difficult to test part would be covered by an integration or acceptance test

If the main loop in your game only goes around once, this will be immediately obvious when you run it.

深海夜未眠 2024-11-09 10:18:39

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

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

当然,您只能在规格中嘲笑这一点。

What about mocking the loop so that it gets executed only the number of times you specify ?

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

Of course, you mock this only in your specs.

为人所爱 2024-11-09 10:18:39

我知道这有点老了,但您也可以使用 Yields 方法来伪造一个块并将单个迭代传递给循环方法。这应该允许您测试在循环中调用的方法,而无需实际将其放入无限循环中。

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

I know this is a little old, but you can also use the yields method to fake a block and pass a single iteration to a loop method. This should allow you to test the methods you're calling within your loop without actually putting it into an infinite loop.

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
小情绪 2024-11-09 10:18:39

我几乎总是使用 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/抛出示例:

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' }

I almost always use a catch/throw construct to test infinite loops.

Raising an error may also work, but that's not ideal especially if your loop's block rescue all errors, including Exceptions. If your block doesn't rescue Exception (or some other error class), then you can subclass Exception (or another non-rescued class) and rescue your subclass:

Exception example

Setup

class RspecLoopStop < Exception; end

Test

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 example:

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

When I first tried this, I was concerned that using should_receive a second time on :some_other_method would "overwrite" the first one, but this is not the case. If you want to see for yourself, add blocks to should_receive to see if it's called the expected number of times:

blah.should_receive(:some_other_method) { puts 'received some_other_method' }
嗳卜坏 2024-11-09 10:18:39

我们测试仅在有信号时退出的循环的解决方案是对退出条件方法进行存根,使其第一次返回 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

Our solution to testing a loop that only exits on signals was to stub the exit condition method to return false the first time but true the second time, ensuring the loop is only executed once.

Class with infinite loop:

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

      # do stuff
    end
  end
end

spec testing the behaviour of the loop:

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
夏雨凉 2024-11-09 10:18:39

:) 几个月前我有此查询

简而言之,没有简单的方法来测试这一点。您测试驱动循环的内部结构。然后你将它放入一个循环方法中&手动测试循环是否正常工作,直到发生终止条件。

:) I had this query a few months ago.

The short answer is there is no easy way to test that. You test drive the internals of the loop. Then you plonk it into a loop method & do a manual test that the loop works till the terminating condition occurs.

霊感 2024-11-09 10:18:39

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

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

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

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

希望这有帮助!

The easiest solution I found is to yield the loop one time and than return. I've used mocha here.

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

We're expecting that the loop is actually executed and it's ensured it will exit after one iteration.

Nevertheless as stated above it's good practice to keep the looping method as small and stupid as possible.

Hope this helps!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文