Idiomatically mock OpenURI.open_uri with Minitest

前端 未结 1 1729
悲哀的现实
悲哀的现实 2021-01-16 01:06

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

相关标签:
1条回答
  • 2021-01-16 01:39

    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!

    0 讨论(0)
提交回复
热议问题