使用RSpec和线程在Ruby中测试一个REPL



我使用RSpec来测试一个简单的REPL的行为。REPL只是返回输入,除非输入是"exit",在这种情况下,它终止循环。

为了避免挂起测试运行器,我在一个单独的线程中运行REPL方法。为了确保线程中的代码在我编写期望之前已经执行,我发现有必要包含一个简短的sleep调用。如果我删除它,测试会间歇性地失败,因为期望有时是在线程中的代码运行之前产生的。

构造代码和规范的好方法是什么,这样我就可以对REPL的行为做出确定性的期望,而不需要sleep黑客?

下面是REPL类和规范:

class REPL
  def initialize(stdin = $stdin, stdout = $stdout)
    @stdin = stdin
    @stdout = stdout
  end
  def run
    @stdout.puts "Type exit to end the session."
    loop do
      @stdout.print "$ "
      input = @stdin.gets.to_s.chomp.strip
      break if input == "exit"
      @stdout.puts(input)
    end
  end
end
describe REPL do
  let(:stdin) { StringIO.new }
  let(:stdout) { StringIO.new }
  let!(:thread) { Thread.new { subject.run } }
  subject { described_class.new(stdin, stdout) }
  # Removing this before hook causes the examples to fail intermittently
  before { sleep 0.01 }
  after { thread.kill if thread.alive? }
  it "prints a message on how to end the session" do
    expect(stdout.string).to match(/end the session/)
  end
  it "prints a prompt for user input" do
    expect(stdout.string).to match(/$ /)
  end
  it "echoes input" do
    stdin.puts("foo")
    stdin.rewind
    expect(stdout.string).to match(/foo/)
  end
end

不要让:stdout是StringIO,你可以用Queue来支持它。然后,当您尝试从队列中读取时,您的测试将只是等待,直到REPL将某些内容推送到队列中(即。

require 'thread'
class QueueIO
  def initialize
    @queue = Queue.new
  end
  def write(str)
    @queue.push(str)
  end
  def puts(str)
    write(str + "n")
  end
  def read
    @queue.pop
  end
end
let(:stdout) { QueueIO.new }

我只是在没有尝试的情况下把它写了出来,它可能不够健壮,不能满足您的需要,但它明白了要点。如果您使用这样的数据结构来同步两个线程,那么您根本不需要休眠。由于这消除了非确定性,因此您不应该看到间歇性故障。

我在这种情况下使用了running?保护。你可能无法完全避免睡眠,但你可以避免不必要的睡眠。

首先,在REPL类中添加一个running?方法。

class REPL
  ...
  def running?
    !!@running
  end
  def run
    @running=true
    loop do
      ...
      if input == 'exit
        @running = false
        break
      end
      ...
    end
  end
end

然后,在你的规格中,休眠直到REPL运行:

describe REPL do
  ...
  before { sleep 0.01 until REPL.running? }
  ...
end

最新更新