用 Minitest 惯用地模拟 OpenURI.open_uri

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 调用成功。我可以把它存根,但我希望有一种方法可以让调用链更接近开始。

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

A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls. A test spy can be an anonymous function or it can wrap an existing function.

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

对于Minitest我们可以使用gemspy.

在我们的测试环境中installing, and including之后,测试可以重排如下:

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 typing 性质,您实际上不需要在测试中提供将在非测试 运行 中创建的确切对象你的应用程序。

让我们看看你的UnderTest class:

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

事实上,"production" 环境中的 open 可以 return Tempfile 的实例,quacks 方法 read。然而,在您的 "test" 环境中,当 "stubbing" 时,您不需要提供 "real" 类型 Tempfile 的对象。提供 任何东西庸医 就足够了。

我在这里使用了 OpenStruct 的力量来构建 东西 ,它将响应 read 消息。让我们仔细看看:

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

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

(2):我们正在 Kernel 模块中的 open 方法上创建一个 spy,这可能会造成混淆,因为我们需要 OpenURI 模块.当我们尝试时:

Spy.on_instance_method(OpenURI, :open)

它抛出异常,

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

原来 open 方法附加到提到的 Kernel 模块。

此外,我们定义了return通过使用以下代码的方法调用编辑的内容:

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.

我们用“http://google.com", but asserting if spy registered call with "http://yahoo.com”参数调用了 get_request

这证明我们的 spy 按预期工作。

答案很长,但我尽力提供了最好的解释,但我不希望所有事情都清楚 - 如果您有任何问题,我非常乐意提供帮助,并更新答案因此!

祝你好运!

我对类似问题的解决方案:

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

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

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

在你的代码中做

URI.open(link)

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