Rspec:File 的 Tempfile 子类被 File 存根请求拦截

Rspec: Tempfile subclass of File get intercepted by File stub request

我正在向 File 发出存根请求,但由于我在调用 File 之前调用了 Tempfile(这是 File 的子类),Tempfile 正在拦截我定义的存根。

型号:

def download_file
  #...
  begin
    tempfile = Tempfile.new([upload_file_name, '.csv'])
    File.open(tempfile, 'wb') { |f| f.write(result.body.to_s) }
    tempfile.path
  rescue Errno::ENOENT => e
    puts "Error writing to file: #{e.message}"
    e
  end
end

Rspec 例子:

it 'could not write to tempfile, so the report is created but without content' do
  allow(File).to receive(:open).and_return Errno::ENOENT
  response_code = post @url, params: { file_ids: [@file.id] }
  expect(response_code).to eql(200)
  assert_requested :get, /#{@s3_domain}.*/, body: @body, times: 1

  report = @file.frictionless_report
  expect(report.report).to eq(nil)
end

错误:

tempfile

tempfile = Tempfile.new([upload_file_name, '.csv'])

收到 Errno::ENOENT,表示存根将发送到 Tempfile 而不是发送到 File

如何定义存根以转到 File 而不是 Tempfile

无需重新打开 Tempfile,它已经打开并委托给文件。

def download_file
  tempfile = Tempfile.new([upload_file_name, '.csv'])
  tempfile.write(result.body.to_s)
  tempfile.path
# A method has an implicit begin.
rescue Errno::ENOENT => e
  puts "Error writing to file: #{e.message}"
  e
end

然后你就可以模拟 Tempfile.new。请注意 exceptions are raised,而不是 returned。

it 'could not write to tempfile, so the report is created but without content' do
  # Exceptions are raised, not returned.
  allow(Tempfile).to receive(:new)
    .and_raise Errno::ENOENT

  response_code = post @url, params: { file_ids: [@file.id] }
  expect(response_code).to eql(200)
  assert_requested :get, /#{@s3_domain}.*/, body: @body, times: 1

  report = @file.frictionless_report
  expect(report.report).to eq(nil)
end

然而,这仍然很脆弱glass-box testing。您的测试了解实现,如果实现发生变化,则测试会给出假阴性。它仍然希望嘲笑 Tempfile.new 不会破坏其他东西。

相反,extract 从 download_file 创建临时文件。

private def new_temp_file_for_upload
  Tempfile.new([upload_file_name, '.csv'])
end

def download_file
  tempfile = new_temp_file_for_upload
  tempfile.write(result.body.to_s)
  tempfile.path
rescue Errno::ENOENT => e
  puts "Error writing to file: #{e.message}"
  e
end

现在模拟可以针对特定对象中的特定方法。我们可以应用一些 good rspec patterns.

context 'when the Tempfile cannot be created' do
  # Here I'm assuming download_file is part of the Controller being tested.
  before do
    allow(@controller).to receive(:new_temp_file_for_upload)
      .and_raise Errno::ENOENT
  end

  it 'creates the report without content' do
    post @url, params: { file_ids: [@file.id] }

    expect(response).to have_http_status(:success)
    assert_requested :get, /#{@s3_domain}.*/, body: @body, times: 1

    report = @file.frictionless_report
    expect(report.report).to be nil
  end
end

注意:return在内部失败后显示“成功”和空报告可能是不正确的。它应该 return 一个 5xx 错误,这样用户无需查看内容就知道发生了故障。

download_file 做的事情太多了。它既是下载文件又是决定如何处理特定错误。它应该只下载文件。让调用堆栈中更高层的东西决定如何处理异常。只做一件事的方法更简单、更灵活、更容易测试且错误更少。

private def new_temp_file_for_upload
  Tempfile.new([upload_file_name, '.csv'])
end

def download_file
  tempfile = new_temp_file_for_upload
  tempfile.write(result.body.to_s)
  tempfile.path
end
context 'when the download fails' do
  before do
    allow(@controller).to receive(:download_file)
      .and_raise "krunch!"
  end

  it 'responds with an error' do
    post @url, params: { file_ids: [@file.id] }

    expect(response).to have_http_status(:error)
  end
end

请注意,不需要特定的错误。 download_file 引发异常就足够了。除了知道 download_file 被调用之外,这个测试现在不知道内部结构。