Idiomatically mock OpenURI.open_uri with Minitest

元气小坏坏 提交于 2019-12-04 02:03:54

问题


I have code that invokes OpenURI.open_uri and I want to confirm the URI being used in the call (so a stub isn't going to work for me), but also intercept the call. I'm hoping not to have to abstract away the call to OpenURI.open_uri just for test purposes. What I've come up with seems verbose and overly complicated.

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

Am I misusing or misunderstanding Minitest's mocking?

Part of the dancing around is that I'm also creating a Tempfile so that my read call succeeds. I could stub that out but I'm hoping there's a way I could head off the call chain closer to the beginning.


回答1:


For this problem, test-spies could be the way to go:

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.

taken from: http://sinonjs.org/docs/

For Minitest we can use gem spy.

After installing, and including it in our test environment, the test can be rearranged as follows:

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): Because of duck typing nature of Ruby, you don't really need to provide in your tests exact objects that would be created in non-test run of your application.

Let's take a look at your UnderTest class:

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

In fact, open in "production" environment could return instance of Tempfile, which quacks with method read. However in your "test" environment, when "stubbing", you don't need to provide "real" object of type Tempfile. It is enough, to provide anything, that quacks like one.

Here I used the power of OpenStruct, to build something, that will respond to read message. Let's take a look at it closer:

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

In our test case we're providing the minimal amount of code, to make the test pass. We don't care about other Tempfile methods, because our tests rely only on read.

(2): We're creating a spy on open method in Kernel module, which might be confusing, because we're requiring OpenURI module. When we try:

Spy.on_instance_method(OpenURI, :open)

it throws exception, that the

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

It turns that the open method is attached to mentioned Kernel module.

Additionally, we define what will be returned by method call with following code:

and_return { OpenStruct.new(read: mock_json) }

When our test script executes, the @test_under.get_request(test_uri) is performed, which registers the open method call with its arguments on our spy object. This is something thah we can assert by (3).

Test what can go wrong

Ok, for now we've seen that our script proceeded without any problems, but I'd like to highlight the example of how assertion on our spy could fail.

Let's modify a bit the test:

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

Which when run, will fail with something similar to:

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

We have called our get_request, with "http://google.com", but asserting if spy registered call with "http://yahoo.com" argument.

This proves our spy works as expected.

It's quite long answer, but I tried to provide the best possible explanation, however I don't expect all things are clear - if you have any questions, I'm more than happy to help, and update the answer accordingly!

Good luck!



来源:https://stackoverflow.com/questions/28813062/idiomatically-mock-openuri-open-uri-with-minitest

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!