manastech / webmock.cr

Mock HTTP::Client
MIT License
102 stars 20 forks source link

Need to "consume" stub after first matched request #20

Open aisrael opened 6 years ago

aisrael commented 6 years ago

This is related to issue https://github.com/vonKingsley/hi8.cr/issues/9

Background

HI8 will create two stubs for two consecutive HTTP interactions ("episodes") with identical request signatures.

However, when HI8 starts playing back requests, Webmock will match the second request with the first stub.

Expected Behaviour

  1. Create stub for e.g. "GET localhost:3000/api", with response.body == "1"
  2. Create second stub for same request, with response.body == "2"
  3. On first call, WebMock will dutifully stub the request and return the first response.
  4. On second call, WebMock will stub the request using the second stub, returning the second response.

Actual Behaviour

  1. On second call, WebMock matches the request to the first stub, returning the first response.

Possible Solutions

after_request callback

If WebMock.cr had an after_request callback, it might be possible to tell WebMock to remove the first stub after the first request. For that to happen, we could either yield the StubRegistry to the callback, or provide a class getter WebMock.registry for @@registry.

Provide option WebMock.consume_stub(request) and WebMock.consume_after_match? option

We could set a boolean option e.g. WebMock.consume_after_match = true, and in core_ext.cr#L11 add e.g.

stub = if WebMock.consume_after_match?
  WebMock.consume_stub(request)
else
  WebMock.find_stub(request)
end

Other options might be specifying a numeric TTL per stub indicating how many times it should be used to match a request before it's discarded/consumed.

aisrael commented 6 years ago

I'd really like to have this resolved, so I can perform proper hi8 testing of my code that does repeated API calls (with the same "signature") to an external Web service.

To wit: all the interactions are being recorded properly. It's just that the way hi8 sets up the mocks for testing, it does it all at once before the test. Hence, subsequent calls to the same URL with the same headers get matched/mocked to the first call.

I'd be more than willing to dive into this and submit a pull request if there are no objections?

bcardiff commented 6 years ago

Hi @aisrael, I think that the sequencing you are trying to achieve can be done with a single stub using a block and letting the logic of which is the expected response to the block.

Isn't that enough to support your use case without changing this shard?

aisrael commented 6 years ago

I think it could, if I wasn't using Hi8. My issue is that stubbing requires you to know all the calls (and their exact signatures) beforehand, then you have to setup the stubs beforehand, and all that. What Hi8 (VCR) does is simply record all HTTP interactions once on first run, then play them back on subsequent runs.

Also, I've checked with Ruby's own webmock gem and webmock does "consume" requests after they're matched.

I realize I can probably get by with simply "monkey-patching" your shard in every project we have that uses it... but that's not ideal either as the number of projects grows.

bcardiff commented 6 years ago

Ok, I see. I am not familiar with vcr semantics, but if the stubs are removed after match I guess that it does not enforce the order in the trace, right? If it should then the consume_after_match is not enough.

aisrael commented 6 years ago

That's true, and the thought did occur to me earlier as I was thinking of other ways to accomplish this (one way is to require Hi8 itself to keep track of all calls matching the same signature, then manage its own queue, removing stubs/responses as they're matched).

At least, for my current use cases it does work: Hi8 records the calls as they occur. Then, during mocking, it calls Webmock.stub for each interaction. That in turn appends each stub (regardless of signature) to StubRegistry into the ivar @stubs (which is an Array(Stub)).

Then, Hi8/Webmock attempts to match each call to a with StubRegistry#find_stub (which uses Array#find) which does perform a linear search. On a match, with consume_after_match we just Array#delete the found Stub. Thus, the order of the calls are preserved.