我使用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