在测试永无止境的进程时,如何从测试中停止进程?



我正在用 Ruby 开发一个长期运行的程序。我正在为此编写一些集成测试。这些测试需要在启动程序后终止或停止程序;否则测试挂起。

例如,使用文件bin/runner

#!/usr/bin/env ruby
while true do
puts "Hello World"
sleep 10
end

(集成)测试将是:

class RunReflectorTest < TestCase
test "it prints a welcome message over and over" do
out, err = capture_subprocess_io do
system "bin/runner"
end
assert_empty err
assert_includes out, "Hello World"
end
end

只是,很明显,这是行不通的;测试开始并且永远不会停止,因为system调用永远不会结束。

我应该如何解决这个问题?问题是否出在system本身,Kernel#spawn会提供解决方案吗?如果是这样,如何?不知何故,以下内容使out为空:

class RunReflectorTest < TestCase
test "it prints a welcome message over and over" do
out, err = capture_subprocess_io do
pid = spawn "bin/runner"
sleep 2
Process.kill pid
end
assert_empty err
assert_includes out, "Hello World"
end
end

.这个方向似乎也会导致很多时间问题(和缓慢的测试)。理想情况下,读取器将遵循 STDOUT 流,并在遇到字符串后立即让测试通过,然后立即终止子进程。我找不到如何使用Process做到这一点.

测试行为,而不是语言功能

首先,您正在做的是TDD反模式。测试应该关注方法或对象的行为,而不是循环等语言功能。如果必须测试循环,请构造一个测试来检查有用的行为,例如"输入无效响应会导致重新提示"。检查循环是否永远循环几乎没有任何用处。

但是,您可能决定通过检查以下内容来测试长时间运行的进程:

  1. 如果它在t时间后仍在运行。
  2. 如果至少执行i次迭代。
  3. 如果循环在给定某些输入或达到边界条件时正确退出。

使用超时或信号结束测试

其次,如果你决定无论如何都要这样做,你可以用Timeout::timeout来逃避这个块。例如:

require 'timeout'
# Terminates block
Timeout::timeout(3) { `sleep 300` }

这是快速和容易的。但是,请注意,使用超时实际上并不表示该过程。如果运行几次,您会注意到睡眠仍作为系统进程运行多次。

最好是当你想用 Process::kill 退出时发出信号,确保你自己清理干净。例如:

pid = spawn 'sleep 300'
Process::kill 'TERM', pid
sleep 3
Process::wait pid

除了资源问题之外,当您生成有状态的东西并且不想污染测试的独立性时,这是一种更好的方法。只要有可能,您几乎总是应该在测试拆解中终止长时间运行(或无限)的进程。

理想情况下,读者会跟随 STDOUT 流,并在遇到字符串时立即让测试通过,然后立即终止子进程。我找不到如何使用进程执行此操作。

您可以通过指定选项将生成进程的 stdout 重定向到任何文件描述符out

pid = spawn(command, :out=>"/dev/null") # write mode

文档

重定向示例

根据 CodeGnome 关于如何使用Timeout::timeout的答案和 andyconhin 关于如何重定向 IO 的答案Process::spawn,我想出了两个 Minitest 助手,可以按如下方式使用:

it "runs a deamon" do
wait_for(timeout: 2) do
wait_for_spawned_io(regexp: /Hello World/, command: ["bin/runner"])
end
end

帮助者是:

def wait_for(timeout: 1, &block)
Timeout::timeout(timeout) do
yield block
end
rescue Timeout::Error
flunk "Test did not pass within #{timeout} seconds"
end
def wait_for_spawned_io(regexp: //, command: [])
buffer = ""
begin
read_pipe, write_pipe = IO.pipe
pid = Process.spawn(command.shelljoin, out: write_pipe, err: write_pipe)
loop do
buffer << read_pipe.readpartial(1000)
break if regexp =~ buffer
end
ensure
read_pipe.close
write_pipe.close
Process.kill("INT", pid)
end
buffer
end

这些可以在测试中使用,它允许我启动一个子进程,捕获 STDOUT,一旦它与测试正则表达式匹配,它就会通过,否则它将等待 '直到超时并失败(测试失败)。

loop将捕获输出并在看到匹配的输出后通过测试。它使用IO.pipe因为对于子进程(及其子进程)来说,这是最透明的。

我怀疑这是否适用于Windows。它需要对IMO做得有点太多的wait_for_spawned_io进行一些清理。安托厄的问题是,Process.kill('INT')可能无法到达孤儿,但在测试运行后仍在运行的孩子。我需要找到一种方法来确保整个进程子树被杀死。

最新更新