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 c
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!