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.
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