【问题标题】:How to test for stdout that is inside a .each iteration block, using rspec?如何使用 rspec 测试 .each 迭代块内的标准输出?
【发布时间】:2021-03-21 11:02:41
【问题描述】:

我有一个示例代码:

  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 块内完成的,因此方法本身返回数组数字范围。

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

谢谢!

【问题讨论】:

  • 你的initialize_program方法在哪里,请放在那里。
  • 对不起,initialize_program实际上应该是上面的print_stuff方法。但我已经解决了。我刚刚从控制台捕获了 STDOUT 并转换为字符串,并对其进行了测试。不过,谢谢,
  • 如果你想使用output匹配器,你需要将一个块传递给expect,所以我认为更改为expect { print_stuff(1, 3) }.to output(...)应该适合你(注意花括号)
  • @StefanRendevski output_to_stdout matcher 可以工作,但在这种情况下,它感觉像是一种反模式。将方法重构为更易于测试,而不是解析标准输出,似乎是一种更简洁的方法。不过,匹配器当然是一个可行的选择。

标签: ruby unit-testing rspec iteration stdout


【解决方案1】:

Todd's answer 很好,我强烈建议您仔细阅读它:重构您的应用程序,使 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,并用它替换了 global var $stdout。

【讨论】:

    【解决方案2】:

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

    这种类型的代码是您应该编写测试的原因首先。您实际上是在测试 Kernel#puts,它始终返回 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
    

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

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

    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
    

    在 IRB 中测试 RSpec 示例

    在 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)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-14
      • 1970-01-01
      • 2019-03-29
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多