我怎样才能适当地模拟出 returns 产生的方法?

How can I appropriately mock out a method that returns yield?

在 Ruby 中,采用块看起来像这样的方法相当常见:

class File
  def open(path, mode)
    perform_some_setup
    yield
  ensure
    do_some_teardown
  end
end

方法看起来像这样也相当地道:

def frobnicate
  File.open('/path/to/something', 'r') do |f|
    f.grep(/foo/).first
  end
end

我想为此编写一个不会影响文件系统的规范,以确保它从文件中提取正确的词,例如:

describe 'frobnicate' do
  it 'returns the first line containing the substring foo' do
    File.expects(:open).yields(StringIO.new(<<EOF))
      not this line
      foo bar baz
      not this line either
    EOF
    expect(frobnicate).to match(/foo bar baz/)  
  end
end

这里的问题是,通过模拟对 File.open 的调用,我还删除了它的 return 值,这意味着 frobnicate 将 return nil。但是,如果我向链中添加 File.returns('foo bar baz') 之类的东西,我最终会得到一个实际上不会命中任何我感兴趣的代码的测试; frobnicate 中的块内容可以做任何事情,测试仍然会通过。

如何在不影响文件系统的情况下适当地测试我的 frobnicate 方法?我并不特别依赖任何特定的测试框架,所以如果你的答案是 "use this awesome gem that'll do it for you" 那么我同意。

看来您只需要稍微不同地模拟对 File 的调用即可。我收到语法错误 运行 你的代码原样,所以我不确定你使用的是哪个版本的 RSpec,但是如果你使用的是 3.x,这将执行工作:

frobnicate_spec.rb

gem 'rspec', '~> 3.4.0'
require 'rspec/autorun'

RSpec.configure do |config|
  config.mock_with :rspec
end

def frobnicate
  File.open('/path/to/something', 'r') do |f|
    f.grep(/foo/).first
  end
end

RSpec.describe 'frobnicate' do
  it 'returns the first line containing the substring foo' do
    allow(File).to receive(:open).and_call_original
    allow(File).to receive(:open).and_yield StringIO.new <<-EOF
      not this line
      foo bar baz
      not this line either
    EOF
    expect(frobnicate).to match(/foo bar baz/)
  end
end

调用 ruby frobnicate_spec.rb 以便我们可以使用指定的 RSpec 版本。

来源:RSpec 模拟 expecting messages and yielding responses

使用 minitest 可以像下面我 post 那样完成。我已经添加了整个可运行文件,因此您可以使用 ruby -Ilib:test test_file.rb:

从命令行对其进行测试
def frobnicate
  found_string = nil
  File.open('/path/to/something', 'r') do |f|
    found_string = f.grep(/foo/).first
  end
  found_string
end

class FrabnicateTest < Minitest::Test
  def test_it_works
    mock_file = StringIO.new(%(
      not this line
      foo bar baz
      not hthis line either
    ))
    search_result = nil
    File.stub(:open, nil, mock_file) do
      search_result = frobnicate
    end
    assert_match(/foo bar baz/, search_result)
  end
end