如何使用 rspec 测试 .each 迭代块内的标准输出?

How to test for stdout that is inside a .each iteration block, using rspec?

我有一个示例代码:

  def print_stuff(first_num, second_num)
    puts 'hello'
    (first_num..second_num).to_a.each do |num|
      puts 'The current number is: '
      puts "#{num}"
    end
  end

我和使用rspec,我想测试一下输出是否正确。我的尝试如下:

  expect(initialize_program(1, 3)).to output(
    "The current number is: 
    1
    The current number is: 
    2
    The current number is: 
    3").to_stdout

但是相反,我得到了一个输出到标准输出的预期块,但不是块错误,因为 initialize_program(1,3) 正在输出文本,但它是在 .each 块内完成的,因此方法本身 returns 数字范围数组。

如何测试块内的文本,看看输出的文本是否正确?

谢谢!

重构以检查字符串,不是标准输出

这种类型的代码是您应该首先编写测试的原因。您实质上是在测试 Kernel#puts,它总是 returns nil,而不是验证您是否构建了您期望的字符串。不要那样做。相反,像这样重构:

def print_stuff(num1, num2)
  str =
    (num1..num2).map { |num|"The current number is: #{num}" }
    .join "\n"
  puts str
  str
end

print_stuff 1, 3
#=> "The current number is: 1\nThe current number is: 2\nThe current number is: 3"

这不仅会在标准输出上打印您期望的内容:

The current number is: 1
The current number is: 2
The current number is: 3

但也会使用方法最后一行的隐式 return 来 return 一个可以用来与规范中的预期进行比较的值。

您还可以将方法重构为 return 字符串对象数组,或者您可能明确想要测试的任何其他内容。您的真实方法越能反映您计划测试的内容越好。

RSpec 例子

RSpec.describe '#print_stuff' do
  it 'prints the expected message' do
    expected_string = <<~EOF
      The current number is: 1
      The current number is: 2
      The current number is: 3
    EOF
    expect(print_stuff 1, 3).to eql(expected_string.chomp)
  end

  # Even without the collection matchers removed in RSpec 3,
  # you can still validate the number of items returned.
  it 'returns the expected number of lines' do
    lines = print_stuff(1, 3).split("\n").count
    expect(lines).to eql(3)
  end
end

测试 RSpec IRB 中的示例

在 irb 中,您可以像这样验证您的规范:

require 'rspec'
include RSpec::Matchers

expected_string = <<~EOF
  The current number is: 1
  The current number is: 2
  The current number is: 3
EOF

# String#chomp is needed to strip the newline from the
# here-document
expect(print_stuff 1, 3).to eql(expected_string.chomp)

# test the returned object in other ways, if you want
lines = print_stuff(1, 3).split("\n").count
expect(lines).to eql(3)

很好,我强烈建议您仔细阅读:以 UI(在您的情况下为 CLI)最小化且易于测试的方式重构您的应用程序。但是当你想要全面覆盖时,你最终需要测试标准输出。

您现在的使用方式:

expect(initialize_program(1, 3)).to output("whatever").to_stdout

意味着当匹配器被调用时 initialize_program(1, 3) 会立即被评估,这太快了 - 它必须先做它的魔法 (*),然后 运行 你的代码。像这样尝试:

expect { initialize_program(1, 3) }.to output("whatever").to_stdout

现在,您传递一个“知道如何”调用 initialize_program(1, 3) 的块,而不是将调用 initialize_program(1, 3) 的结果传递给匹配器。那么匹配器做了什么:

  1. saves the block for later
  2. 捕获任何进入 STDOUT 的内容是否神奇(见下文)
  3. calls the block,调用 initialize_program(1, 3),捕获应该去 STDOUT 的任何内容
  4. compares it 与您期望的设置(output("whatever") 部分)

https://relishapp.com/rspec/rspec-expectations/docs/built-in-matchers/output-matcher

  • 提到的魔法反正不是那么神奇:

https://github.com/rspec/rspec-expectations/blob/44d90f46a2654ffeab3ba65eff243039232802ce/lib/rspec/matchers/built_in/output.rb#L49https://github.com/rspec/rspec-expectations/blob/44d90f46a2654ffeab3ba65eff243039232802ce/lib/rspec/matchers/built_in/output.rb#L141

它只是创建 StringIO,并用它替换全局变量 $stdout。