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

时间:2017-06-16 14:52:31

标签: ruby testing spawn

我正在用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

执行此操作

3 个答案:

答案 0 :(得分:1)

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

首先,您正在做的是TDD反模式。测试应该关注方法或对象的行为,而不是像循环这样的语言特性。如果您必须测试循环,请构建一个测试,检查有用的行为,例如&#34;输入无效的响应会导致重新提示。&#34;在检查循环永远循环时几乎没有用处。

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

  1. 如果它仍然在 t 之后运行。
  2. 如果它至少执行了 i 次迭代。
  3. 如果在给定某些输入或达到边界条件时循环正确退出。
  4. 使用超时或信号结束测试

    其次,如果你决定这样做,你可以用Timeout::timeout来逃避阻止。例如:

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

    这很快捷。但请注意,使用超时并不能实际发出信号。如果您运行几次,您会注意到 sleep 仍然作为系统进程多次运行。

    当您想要使用Process::kill退出时,最好发出信号,确保您自己清理完毕。例如:

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

    除了资源问题之外,当您正在产生有状态并且不想污染测试的独立性时,这是一种更好的方法。你应该尽可能地在你的测试拆解中杀死长时间运行(或无限)的进程。

答案 1 :(得分:1)

  

理想情况下,读者会遵循STDOUT流并在遇到字符串后立即通过测试,然后立即终止子进程。我无法通过Process找到如何做到这一点。

您可以通过指定out选项

将生成的进程的stdout重定向到任何文件描述符
pid = spawn(command, :out=>"/dev/null") # write mode

Documentation

Example of redirection

答案 2 :(得分:0)

关于如何使用Timeout::timeoutanswer from CodeGnome关于如何重定向Process::spawn IO的the answer from andyconhin,我提出了两个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上运行。它需要对wait_for_spawned_io进行一些清理工作,而IMO的工作量略微过高。另一个问题是,Process.kill('INT')可能无法到达孤儿的孩子,但在此测试运行后仍然在运行。我需要找到一种方法来确保整个进程子树被杀死。