{"id":5747,"date":"2013-10-03T12:31:45","date_gmt":"2013-10-03T09:31:45","guid":{"rendered":"http:\/\/railsware.com\/blog\/?p=5747"},"modified":"2017-12-21T03:16:21","modified_gmt":"2017-12-21T00:16:21","slug":"custom-vcr-matchers-for-dealing-with-mutable-http-requests","status":"publish","type":"post","link":"https:\/\/railsware.com\/blog\/custom-vcr-matchers-for-dealing-with-mutable-http-requests\/","title":{"rendered":"Custom VCR matchers for dealing with mutable HTTP-requests"},"content":{"rendered":"<a href=\"https:\/\/www.relishapp.com\/vcr\/vcr\/docs\">VCR<\/a> is a powerful beast which makes HTTP-request mocking a real no-brainer. If you haven&#8217;t tried it before, just do it, it will make your life much easier.\r\n\r\nVCR works in really simple manner &#8211; when you issue request via some HTTP API, at first it records them and on second request reuse information stored from the first request. The tricky part of such behavior &#8211; how to match new request to the one stored before. Usually it just works, but when you have some mutable or random part in an URL it becomes a little bit harder.\r\n\r\nIn this article you will learn how VCR request-matching works and how to customise matching strategy to deal with mutable URI.\r\n<h2>Predefined strategies<\/h2>\r\nVCR has next predefined matchers:\r\n<ul>\r\n \t<li><strong>:method<\/strong> &#8211; HTTP method (i.e. GET, POST, PUT or DELETE) of the request<\/li>\r\n \t<li><strong>:uri<\/strong> &#8211; full URI of the request<\/li>\r\n \t<li><strong>:host<\/strong> &#8211; host of the URI(without port)<\/li>\r\n \t<li><strong>:path<\/strong> &#8211; path of the URI<\/li>\r\n \t<li><strong>:query<\/strong> &#8211; query string values of the URI<\/li>\r\n \t<li><strong>:body<\/strong> &#8211; body of the request(PUT or POST methods)<\/li>\r\n \t<li><strong>:headers<\/strong> &#8211; request headers<\/li>\r\n<\/ul>\r\nYou can combine any of these methods to obtain required behavior. By default VCR matches request using <strong>:method<\/strong> and <strong>:uri<\/strong>. For plane GET request it&#8217;s enough.\r\n\r\nStrict <strong>:uri<\/strong> matching can be substituted with combination of [<strong>:host<\/strong>, <strong>:path<\/strong>] in case you have some dynamic query part, which doesn&#8217;t important for you(e.g. ?timestamp=123). Combination of [<strong>:host<\/strong>, <strong>:query<\/strong>] can be used if you have dynamic path in URI. But don&#8217;t use such combinations, there are much better options, which will be described later.\r\n\r\nMatcher for <strong>:body<\/strong> is handy for stricter matching of POST\/PUT requests, and matcher on <strong>:headers<\/strong> when HTTP-headers convey some important data.\r\n<h2>Variable query part<\/h2>\r\nThe most common case for mutable URI &#8211; is some timestamp or another random data in query-part of request. VCR provides special helper for such cases. Use:\r\n<pre lang=\"ruby\"># for ignoring one parameter\r\nVCR.request_matchers.uri_without_param(:timestamp)\r\n\r\n# for ignoring several parameters\r\nVCR.request_matchers.uri_without_params(:timestamp, :session)\r\n<\/pre>\r\nUsing this helper VCR matches on the whole URI, but excludes :timestamp(and :session) from matching in query-part of URI. Here is an example of usage in RSpec:\r\n<pre lang=\"ruby\">describe \"#fetch_info\", vcr: {match_requests_on: [:method,                  \r\n                              VCR.request_matchers.uri_without_param(:timestamp)]} do\r\n  # ...\r\nend\r\n<\/pre>\r\n<h2>Variable path part<\/h2>\r\nSometimes, especially for REST-API, where you dynamically generate user_id in test and do request to remote server, request path can vary on ID. To skip matching on variable ID, you can write custom URI-matcher. In fact, this way can be used(and should be) when you can&#8217;t assemble required matching strategy from standard VCR-matchers.\r\n\r\nCustom matcher can be any Proc-object which accepts two VCR::Request objects. Here is an implementation for trailing ID URI:\r\n<pre lang=\"ruby\"># URI example: \r\n# http:\/\/example.com\/users\/123\r\n# matches to\r\n# http:\/\/example.com\/users\/124\r\nuri_ignoring_trailing_id = lambda do |request_1, request_2|\r\n  uri1, uri2 = request_1.uri, request_2.uri\r\n  regexp_trail_id = %r(\/\\d+\/?\\z)\r\n  if uri1.match(regexp_trail_id)\r\n    r1_without_id = uri1.gsub(regexp_trail_id, \"\")\r\n    r2_without_id = uri2.gsub(regexp_trail_id, \"\")\r\n    uri1.match(regexp_trail_id) &amp;&amp; uri2.match(regexp_trail_id) &amp;&amp; r1_without_id == r2_without_id\r\n  else\r\n    uri1 == uri2\r\n  end\r\nend\r\n\r\ndescribe \"#upgrade\", vcr: {match_requests_on: [:method, uri_ignoring_trailing_id]} do\r\n  #...\r\nend\r\n<\/pre>\r\nNotice, we wrote matcher like variable, not like symbol.\r\n\r\nRequest-object has next API:\r\n<ul>\r\n \t<li><strong>#method<\/strong> &#8211; symbol(:put, :post, etc)<\/li>\r\n \t<li><strong>#uri<\/strong> &#8211; string, full non-parsed URI<\/li>\r\n \t<li><strong>#body<\/strong> &#8211; string, POST\/PUT request body<\/li>\r\n \t<li><strong>#headers<\/strong> &#8211; Hash, all request headers<\/li>\r\n \t<li><strong>#parsed_uri<\/strong> &#8211; URI-object(with scheme, host, port, path, and query accessors)<\/li>\r\n<\/ul>\r\nThis provides all means to write any custom matching strategy. VCR doesn&#8217;t have :port matcher and it can be easily implemented via &#8220;parsed_uri&#8221;-method:\r\n<pre lang=\"ruby\">port = lambda do |r1, r2|\r\n  r1.parsed_uri.port == r2.parsed_uri.port\r\nend\r\n\r\ndescribe \"#remote_call\", vcr: {match_requests_on: [:method, :host, port]} do\r\n  #...\r\nend\r\n<\/pre>\r\n<h2>Variable response<\/h2>\r\nCustom matchers work great if response body isn&#8217;t parametrized by request information. If this is a case, you should leverage VCR callbacks and it can be a subject for the whole new article. But there is a good example of such usage at <a href=\"https:\/\/github.com\/nbibler\/vcr-284\">https:\/\/github.com\/nbibler\/vcr-284<\/a>. It can hint you with required approach.\r\n\r\nAnyway, if stuff becomes too complex, it&#8217;s much easier to directly use webmock, fakeweb or any other mocking framework which is used by your configuration of VCR.\r\n<h2>Move custom matchers into configuration<\/h2>\r\nIn case, you want to reuse custom matcher, it can be put in RSpec configuration:\r\n<pre lang=\"ruby\">VCR.configure do |c|\r\n  c.register_request_matcher :uri_without_timestamp, &amp;VCR.request_matchers.uri_without_param(:timestamp)\r\n\r\n  c.register_request_matcher :port do |r1, r2|\r\n    r1.parsed_uri.port == r2.parsed_uri.port\r\n  end\r\n\r\n  c.register_request_matcher :uri_ignoring_trailing_id do |request_1, request_2|\r\n    uri1, uri2 = request_1.uri, request_2.uri\r\n    regexp_trail_id = %r(\/\\d+\/?\\z)\r\n    if uri1.match(regexp_trail_id)\r\n      r1_without_id = uri1.gsub(regexp_trail_id, \"\")\r\n      r2_without_id = uri2.gsub(regexp_trail_id, \"\")\r\n      uri1.match(regexp_trail_id) &amp;&amp; uri2.match(regexp_trail_id) &amp;&amp; r1_without_id == r2_without_id\r\n    else\r\n      uri1 == uri2\r\n    end\r\n  end\r\nend\r\n<\/pre>\r\nNow you can use :uri_without_timestamp, :port, and :uri_ignoring_trailing_id in tests:\r\n<pre lang=\"ruby\">describe \"#fetch_info\", vcr: {match_requests_on: [:method, :uri_without_timestamp]} do\r\n  #...\r\nend\r\n\r\ndescribe \"#remote_call\", vcr: {match_requests_on: [:method, :host, :port]} do\r\n  #...\r\nend\r\n\r\ndescribe \"#upgrade\", vcr: {match_requests_on: [:method, :uri_ignoring_trailing_id]} do\r\n  #...\r\nend\r\n<\/pre>\r\n<h2>Debug<\/h2>\r\nSometimes, things go wrong and you need a way to sneak into matching process. Drop this code into any test and it enables debug-mode:\r\n<pre lang=\"ruby\">VCR.configure do |c|\r\n  c.debug_logger = STDOUT\r\nend\r\n<\/pre>\r\nNow you are fully armed and ready to use VCR at its fullest.","protected":false},"excerpt":{"rendered":"<p>VCR is a powerful beast which makes HTTP-request mocking a real no-brainer. If you haven&#8217;t tried it before, just do it, it will make your life much easier. VCR works in really simple manner &#8211; when you issue request via some HTTP API, at first it records them and on second request reuse information stored&#8230;<\/p>\n","protected":false},"author":25,"featured_media":9450,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[3],"tags":[],"coauthors":["Sergii Boiko"],"class_list":["post-5747","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development"],"acf":[],"aioseo_notices":[],"categories_data":[{"name":"Engineering","link":"https:\/\/railsware.com\/blog?category=development"}],"post_thumbnails":"https:\/\/railsware.com\/blog\/wp-content\/themes\/railsware\/vendors\/images\/article-thumbnail-default.jpg","amp_enabled":true,"_links":{"self":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/5747","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/users\/25"}],"replies":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/comments?post=5747"}],"version-history":[{"count":51,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/5747\/revisions"}],"predecessor-version":[{"id":9505,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/5747\/revisions\/9505"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/media\/9450"}],"wp:attachment":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/media?parent=5747"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/categories?post=5747"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/tags?post=5747"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/coauthors?post=5747"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}