Idiomatically Mock Openuri.Open_Uri with Minitest

Idiomatically mock OpenURI.open_uri with Minitest

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!

Exact argument matches with Minitest mock expectations?

However it seems that matching is based on case equality [===]

...which for the String class is the same as ==.

the second expectation overrides the first.

It appears that the ordering of the expect statements is important with MiniTest::Mock objects:

require 'minitest/autorun'

resp = MiniTest::Mock.new
resp.expect :[], 3141, ["X-Pagination-TotalElements"]
resp.expect :[], 32, ["X-Pagination-TotalPages"]

puts resp["X-Pagination-TotalElements"]
puts resp["X-Pagination-TotalPages"]

--output:--
3141
32
Run options: --seed 29457

# Running:

Finished in 0.001500s, 0.0000 runs/s, 0.0000 assertions/s.

0 runs, 0 assertions, 0 failures, 0 errors, 0 skips

But this fails:

require 'minitest/autorun'

resp = MiniTest::Mock.new
resp.expect :[], 3141, ["X-Pagination-TotalElements"]
resp.expect :[], 32, ["X-Pagination-TotalPages"]

puts resp["X-Pagination-TotalPages"]
puts resp["X-Pagination-TotalElements"]

--output:--

/Users/7stud/.rvm/gems/ruby-2.1.2/gems/minitest-5.4.0/lib/minitest/mock.rb:148:in
method_missing': mocked method :[] called with unexpected arguments
["X-Pagination-TotalPages"] (MockExpectationError) from 1.rb:7:in
'

And quantity plays a role as well:

require 'minitest/autorun'

resp = MiniTest::Mock.new
resp.expect :[], 3141, ["X-Pagination-TotalElements"]

puts resp["X-Pagination-TotalElements"]
puts resp["X-Pagination-TotalElements"]

--output:--

3141

/Users/7stud/.rvm/gems/ruby-2.1.2/gems/minitest-5.4.0/lib/minitest/mock.rb:122:in
method_missing': No more expects available for :[]:
["X-Pagination-TotalElements"] (MockExpectationError) from 1.rb:7:in
'

As a result, the meaning of:

resp.expect :[], 3141, ["X-Pagination-TotalElements"]
resp.expect :[], 32, ["X-Pagination-TotalPages"]

..is:

  1. I expect that initially calling resp[] with the key "X-Pagination-TotalElements" will return 3141.

  2. I expect that calling resp[] again with the key "X-Pagination-TotalPages" will return 32.

  3. I expect that any additional calls or calls not in that order will result in failure.

...

What is the correct way to accomplish what I am trying to do?

Add the expect statements in the order that they will be called in your code.

How to properly stub doubles

I think, this is not really problem with stubbing, but the general approach. When writing your unit tests for some class, you should stick to functionality of that class and eventually to API it sees. If you're stubbing "internal" out of Interface - it's already to much for specs of Session.

What Session really sees, is Interfaces public hello method, thus Session spec, should not be aware of internal implementation of it (that it is @out.puts "hello"). The only thing you should really focus is that, the hello method has been called. On the other hand, ensuring that the put is called for hello should be described in specs for Interface.

Ufff... That's long introduction/explanation, but how to proceed then? (known as show me the code! too ;)).

Having said, that Session.new should be aware only of Interfaces hello method, it should trust it works properly, and Sessions spec should ensure that the method is called. For that, we'll use a spy. Let's get our hand dirty!

RSpec.describe Session do
let(:fake_interface) { spy("interface") }
let(:session) { Session.new }

before do
allow(Interface).to receive(:new).and_return(fake_interface)
end

describe "#new" do
it "creates an instance of Session" do
expect(session).to be_an_instance_of(Session) # this works now!
end

it "calls Interface's hello method when initialized" do
Session.new
expect(fake_interface).to have_received(:hello)
end
end
end

A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls.

This is taken from SinonJS (which is the first result when googling for "what is test spy"), but explanation is accurate.

How does this work?

Session.new
expect(fake_interface).to have_received(:hello)

First of all, we're executing some code, and after that we're asserting that expected things happened. Conceptually, we want to be sure, that during Session.new, the fake_interface have_received(:hello). That's all!

Ok, but I need another test ensuring that Interfaces method is called with specific argument.

Ok, let's test that!

Assuming the Session looks like:

class Session
def initialize
@interface = Interface.new(self)
@interface.hello
@interface.say "Something More!"
end
end

We want to test say:

RSpec.describe Session do
describe "#new" do
# rest of the code

it "calls interface's say_something_more with specific string" do
Session.new
expect(fake_interface).to have_received(:say).with("Something More!")
end
end
end

This one is pretty straightforward.

One more thing - my Interface takes a Session as an argument. How to test that the interface calls sessions method?

Let's take a look at sample implementation:

class Interface
# rest of the code

def do_something_to_session
@session.a_session_method
end
end

class Session
# ...

def another_method
@interface.do_something_to_session
end

def a_session_method
# some fancy code here
end
end

It won't be much surprise, if I say...

RSpec.describe Session do
# rest of the code

describe "#do_something_to_session" do
it "calls the a_session_method" do
Session.new.another_method
expect(fake_interface).to have_received(:do_something_to_session)
end
end
end

You should check, if Sessions another_method called interfaces do_something_to_session method.

If you test like this, you make the tests less fragile to future changes. You might change an implementation of Interface, that it doesn't rely on put any more. When such change is introduced - you have to update the tests of Interface only. Session knows only the proper method is called, but what happens inside? That's the Interfaces job...

Hope that helps! Please, take a look at another example of spy in my other answer.

Good luck!



Related Topics



Leave a reply



Submit