webmock icon indicating copy to clipboard operation
webmock copied to clipboard

Mention ability to specify chunks in the README

Open janko opened this issue 9 years ago • 9 comments

With WebMock I needed to test streaming response body with net/http, so I wanted to set specific chunks which will be yielded when I run the following code:

uri = URI("http://example.com")
Net::HTTP.get_response(uri) do |response|
  response.read_body { |chunk| ... }
end

I was reading a bit of WebMock source code, but I couldn't find anything that might indicate existence of this feature. So at the end I just tried out the following, knowing that it probably wouldn't work:

stub_request(:get, "http://example.com").to_return(body: ["a", "b", "c"])

But it worked! This made me really happy! I just thought it would be great to document this feature in the README.

Absolutely awesome library, thank you! ❤️

janko avatar Jun 21 '16 10:06 janko

wow, I never thought of that! nice :)

This can be documented as long as we add an acceptance spec to cover that scenario and it passes across all supported http clients.

bblimke avatar Jun 21 '16 10:06 bblimke

Actually, I misread, it only yields only one chunk equal to ["a", "b", "c"] (I used puts instead of p, so I thought I was getting the right result). Maybe this could be a feature, but I guess it would take a lot of work to port it to all other adapters.

janko avatar Jun 21 '16 10:06 janko

In the meanwhile I managed to create a workaround, so posting here in case anyone's interested:

before do
  mocked_http = Class.new(Net::HTTP) do
    def request(*)
      super do |response|
        response.instance_eval do
          def read_body(*, &block)
            @body.each_char(&block)
          end
        end
        yield response if block_given?
        response
      end
    end
  end

  @original_net_http = Net.send(:remove_const, :HTTP)
  Net.send(:const_set, :HTTP, mocked_http)
end

after do
  Net.send(:remove_const, :HTTP)
  Net.send(:const_set, :HTTP, @original_net_http)
end

janko avatar Jun 22 '16 08:06 janko

A slightly modified @janko's example from above, which avoids instance variable in spec, and expects stubbed body to be an array, where each array element will yield the the block passed to read_body:

let(:mock_net_http) do
  Class.new(Net::HTTP) do
    def request(*)
      super do |response|
        response.instance_eval do
          def read_body(*, &)
            @body.each(&)
          end
        end

        yield response if block_given?

        response
      end
    end
  end
end

let(:remove_original_net_http) { Net.send(:remove_const, :HTTP) }
let(:original_http) { remove_original_net_http }
let(:stub_net_http) { Net.send(:const_set, :HTTP, mock_net_http) }

let(:remove_stubbed_net_http) { Net.send(:remove_const, :HTTP) }
let(:restore_net_http) { Net.send(:const_set, :HTTP, original_http) }

before do
  mock_net_http
  remove_original_net_http
  stub_net_http

  # Stub response body as an array of chunks
  stub_request(:post, "OpenAI url").to_return(status: 200, body: ["first chunk", "second chunk"])
end

after do
  remove_stubbed_net_http
  restore_net_http
end

pjg avatar Oct 10 '23 14:10 pjg

@pjg thanks a lot, save my day!

ShallmentMo avatar Nov 28 '23 07:11 ShallmentMo

:hugs: thanks, very helpful wish this was built in.

SamSaffron avatar Dec 11 '23 01:12 SamSaffron

hopes and prayers :) https://meta.discourse.org/discourse-ai/ai-bot/shared-ai-conversations/ZRd74GXAe7KhS1pUqcQ3nw

Will try to wire up a PR tomorrow... who knows how well this will do...

SamSaffron avatar Mar 18 '24 06:03 SamSaffron

For the record... AI was sadly zero help here... made a PR :)

SamSaffron avatar Mar 19 '24 03:03 SamSaffron

Great, thanks!

These hacks got me on the right track to cook up something that works for my environment (ruby 3.3.4, HTTParty 0.22,, rspec 3.13, webmock 3.23) at the moment.

I wanted to keep the hack at my spec_helper instead of inlining in suites. This and/or versions might have caused some differentiating factors on what I had to do. This implementation prolly breaks some things like the case for what the WebMockHTTPResponse module exists in the first place, but now that I can toggle it for specific tests it's good enough for my use case for now.

For what it's worth, here's what I had to put together to get my mocks rocking:

spec_helper.rb

  # patch Net::WebMockHTTPResponse to support chunked responses
  config.around(:example, :chunked_response) do |example|
    original_method = Net::WebMockHTTPResponse.instance_method(:read_body)
    Net::WebMockHTTPResponse.send(:define_method, :read_body) do |*args, &block|
      @body.each(&block)
    end
    example.run
    Net::WebMockHTTPResponse.send(:define_method, :read_body, original_method)
  end

somethinsomethingdarkside_spec.rb

describe "response" do
  let!(:endpoint) { "http://example.com/foshizzlez" }
  let!(:chunks) { %w(it was all a dream used to read word up magazine) }
  before(:each) do
    stub_request(:post, "http://example.com/foshizzlez").with({body: "holla"}).to_return(status: 200, body: chunks)
  end
  it "is chunky", chunked_response: true do
    dada = ""
    HTTParty.post("http://example.com/foshizzlez", body: "holla") do |fragment|
      dada += fragment
    end
    expect(dada).to eq(chunks.join)
  end
end

I hope it helps someone on a similar quest.

yoka avatar Aug 20 '24 16:08 yoka