Idiomatically mock OpenURI.open_uri with Minitest



我有调用OpenURI.open_uri的代码,我想确认调用中使用的URI(所以存根对我来说不起作用),但也要拦截调用。我希望不必为了测试目的而抽象掉对OpenURI.open_uri的调用。我的想法似乎冗长而过于复杂。

under_test.rb

require 'open-uri'
class UnderTest
  def get_request(uri)
    open(uri).read
  end
end

test_under_test.rb

require 'minitest/autorun'
require './lib/under_test'
class TestUnderTest < Mintest::Test
  def test_get_request
    @under_test = UnderTest.new
    mock_json  = '{"json":[{"element":"value"}]}'
    uri = URI('https://www.example.com/api/v1.0?attr=value&format=json')
    tempfile = Tempfile.new('tempfile')
    tempfile.write(mock_json)
    mock_open_uri = Minitest::Mock.new
    mock_open_uri.expect(:call, tempfile, [uri])
    OpenURI.stub :open_uri, mock_open_uri do
      @under_test.get_request('https://www.example.com/api/v1.0?attr=value&format=json'
    end
    mock_open_uri.verify
  end
end

我是否滥用或误解了Minitest的嘲讽?

我还创建了一个Tempfile,这样我的read调用就成功了。我可以把它截住,但我希望有一种方法可以让我在更接近开始的时候摆脱呼叫链。

对于这个问题,测试间谍可能是解决方法:

测试间谍是一个记录所有调用的参数、返回值、this的值以及抛出的异常(如果有的话)的函数。测试间谍可以是匿名函数,也可以包装现有函数。

摘自:http://sinonjs.org/docs/

对于Minitest,我们可以使用gem spy。

安装后,并将其包含在我们的测试环境中,可以按如下方式重新安排测试:

require 'minitest/autorun'
require 'spy/integration'
require 'ostruct' # (1)
require './lib/under_test'
class TestUnderTest < Minitest::Test
  def test_get_request
    mock_json = '{"json":[{"element":"value"}]}'
    test_uri = URI('https://www.example.com/api/v1.0?attr=value&format=json')
    open_spy = Spy.on_instance_method(Kernel, :open) # (2)
                  .and_return { OpenStruct.new(read: mock_json) } # (1)
    @under_test = UnderTest.new
    assert_equal @test_under.get_request(test_uri), mock_json
    assert open_spy.has_been_called_with?(test_uri) # (3)
  end
end

(1) :由于Ruby的duck类型特性,您实际上不需要在测试中提供在应用程序的非测试运行中创建的确切对象。

让我们来看看您的UnderTest类:

class UnderTest
  def get_request(uri)
    open(uri).read
  end
end

事实上,"生产"环境中的open可以返回Tempfile的实例,该实例使用方法read发出嘎嘎声。然而,在您的"测试"环境中,当"存根"时,您不需要提供Tempfile类型的"真实"对象。只要提供任何东西就足够了。

在这里,我使用了OpenStruct的功能,构建了一些,它将响应read消息。让我们仔细看看:

require 'ostruct'
tempfile = OpenStruct.new(read: "Example output")
tempfile.read # => "Example output"

在我们的测试用例中,我们提供了最小的代码量,以使测试通过。我们不关心其他Tempfile方法,因为我们的测试只依赖于read

(2) :我们正在Kernel模块中创建一个关于open方法的间谍,这可能会令人困惑,因为我们需要OpenURI模块。当我们尝试时:

Spy.on_instance_method(OpenURI, :open)

它抛出了一个例外,

NameError: undefined method `open' for module `OpenURI'

结果表明,CCD_ 17方法是附加在上述CCD_ 18模块上的。

此外,我们用以下代码定义了方法调用将返回的内容:

and_return { OpenStruct.new(read: mock_json) }

当我们的测试脚本执行时,执行@test_under.get_request(test_uri),它在spy对象上注册open方法调用及其参数。这是我们可以通过(3)断言的。

测试可能出错的地方

好的,现在我们已经看到我们的脚本没有任何问题,但我想强调一下spy上的断言如何失败的例子。

让我们修改一下测试:

class TestUnderTest < Minitest::Test
  def test_get_request
    open_spy = Spy.on_instance_method(Kernel, :open)
                  .and_return { OpenStruct.new(read: "whatever") }
    UnderTest.new.get_request("http://google.com")
    assert open_spy.has_been_called_with?("http://yahoo.com")
  end
end

当运行时,将失败,类似于:

  1) Failure:
TestUnderTest#test_get_request [test/lib/test_under_test.rb:17]:
Failed assertion, no message given.

我们已致电get_request,并附上"http://google.com",但如果spy使用"http://yahoo.com"参数。

这证明了我们的spy如预期的那样工作。

这是一个很长的答案,但我试图提供最好的解释,但我不希望所有的事情都很清楚——如果你有任何问题,我非常乐意提供帮助,并相应地更新答案!

祝你好运!

我的类似问题解决方案:

URI::HTTPS.any_instance.stubs(:open).returns(file)

使用新的Rspec,您可以简单地使用:

allow(URI).to receive(:open).and_return(your_value)

在你的代码做

URI.open(link)

原因不建议直接在内核上调用open。

最新更新