Record 'HTTP' Calls to Disk

Record test suite 'HTTP' requests and replays them during future runs. A port of the Ruby gem of the same name (< https://github.com/vcr/vcr/>). Works by hooking into the 'webmockr' R package for matching 'HTTP' requests by various rules ('HTTP' method, 'URL', query parameters, headers, body, etc.), and then caching real 'HTTP' responses on disk in 'cassettes'. Subsequent 'HTTP' requests matching any previous requests in the same 'cassette' use a cached 'HTTP' response.


cran checks Project Status: Active – The project has reached a stable, usable state and is being actively developed. Build Status Build status codecov rstudio mirror downloads cran version

An R port of the Ruby gem vcr

Check out the HTTP testing book and the vcr vignettes.

Supported HTTP libraries

Usage

library(vcr)
library(crul)
 
cli <- crul::HttpClient$new(url = "https://eu.httpbin.org")
system.time(
  use_cassette(name = "helloworld", {
    cli$get("get")
  })
)
#>    user  system elapsed 
#>   0.182   0.026   1.302

The request gets recorded, and all subsequent requests of the same form used the cached HTTP response, and so are much faster

system.time(
  use_cassette(name = "helloworld", {
    cli$get("get")
  })
)
#>    user  system elapsed 
#>   0.079   0.003   0.084

Importantly, your unit test deals with the same inputs and the same outputs - but behind the scenes you use a cached HTTP response - thus, your tests run faster.

The cached response looks something like (condensed for brevity):

http_interactions:
request:
    method: get
    uri: https://eu.httpbin.org/get
    body:
      encoding: ''
      string: ''
    headers:
      User-Agent: libcurl/7.54.0 r-curl/3.2 crul/0.5.2
  response:
    status:
      status_code: '200'
      message: OK
      explanation: Request fulfilled, document follows
    headers:
      status: HTTP/1.1 200 OK
      connection: keep-alive
    body:
      encoding: UTF-8
      string: "{\n  \"args\": {}, \n  \"headers\": {\n    \"Accept\"\"application/json,
        text/xml, application/xml, */*\"\n    \"Accept-Encoding\"\"gzip, deflate\",
        \n    \"Connection\"\"close\"\n    \"Host\"\"httpbin.org\"\n    \"User-Agent\":
        \"libcurl/7.54.0 r-curl/3.2 crul/0.5.2\"\n  }, \n  \"origin\"\"111.222.333.444\",
        \n  \"url\"\"https://eu.httpbin.org/get\"\n}\n"
  recorded_at: 2018-04-03 22:55:02 GMT
  recorded_with: vcr/0.1.0, webmockr/0.2.4, crul/0.5.2

All components of both the request and response are preserved, so that the HTTP client (in this case crul) can reconstruct its own response just as it would if it wasn't using vcr.

Terminology

  • vcr: the name comes from the idea that we want to record something and play it back later, like a vcr
  • cassette: A thing to record HTTP interactions to. Right now the only option is file system, but in the future could be other things, e.g. a key-value store like Redis
  • Persisters: defines how to save requests - currently only option is the file system
  • Serializers: defines how to serialize the HTTP response - currently only option is YAML; other options in the future could include e.g. JSON
  • insert cassette: create a cassette (all HTTP interactions will be recorded to this cassette)
  • eject cassette: eject the cassette (no longer recording to that cassette)
  • replay: refers to using a cached result of an http request that was recorded earlier

What does vcr do?

The short version is: vcr helps you stub HTTP requests so you don't have to repeat HTTP requests.

The main use case is for unit tests for R packages.

vcr currently works with the crul and httr packages; support for curl is in the works.

How it works in lots of detail

The Steps

  1. Use either vcr::use_cassette or vcr::insert_cassette a. If you use vcr::insert_cassette, make sure to run vcr::eject_cassette when you're done to stop recording
  2. When you first run a request with vcr there's no cached data to use, so we allow HTTP requests until you're request is done.
  3. Before we run the real HTTP request, we "stub" the request with webmockr so that future requests will match the stub. This stub is an R6 class with details of the interaction (request + response), but is not on disk.
  4. After the stub is made, we run the real HTTP request.
  5. We then disallow HTTP requests so that if the request is done again we use the cached response
  6. The last thing we do is write the HTTP interaction to disk in a mostly human readable form.

When you run that request again using vcr::use_cassette or vcr::insert_cassette:

  • We use webmockr to match the request to cached requests, and since we stubbed the request the first time we used the cached response.

Of course if you do a different request, even slightly (but depending on which matching format you decided to use), then the request will have no matching stub and no cached response, and then a real HTTP request is done - we then cache it, then subsequent requests will pull from that cached response.

webmockr has adapters for each R client (again, right now only crul) - so that we actually intercept HTTP requests when webmockr is loaded and the user turns it on. So, webmockr doesn't actually require an internet or localhost connection at all, but can do its thing just fine by matching on whatever the user requests to match on. In fact, webmockr doesn't allow real HTTP requests by default, but can be toggled off of course.

The main use case we are going for in vcr is to deal with real HTTP requests and responses, so we allow real HTTP requests when we need to, and turn it off when we don't.

This gives us a very flexible and powerful framework where we can support webmockr and vcr integration for any number of R clients for HTTP requests and support many different formats serialized to disk.

Just want to mock and not store on disk?

You're looking for webmockr. webmockr only matches requests based on criteria you choose, but does not cache HTTP interactions to disk as vcr does.


Best practices

vcr for tests

  • Add vcr to Suggests in your DESCRIPTION file (optionally add webmockr, but it's not explicitly needed as vcr will pull it in)
  • Make a file in your tests/testthat/ directory called helper-yourpackage.R (or skip if as similar file already exists). In that file use the following lines to setup your path for storing cassettes (change path to whatever you want):
library("vcr")
invisible(vcr::vcr_configure())
  • In your tests, for whichever tests you want to use vcr, wrap them in a vcr::use_cassette() call like:
library(testthat)
vcr::use_cassette("rl_citation", {
  test_that("my test", {
    aa <- rl_citation()
 
    expect_is(aa, "character")
    expect_match(aa, "IUCN")
    expect_match(aa, "www.iucnredlist.org")
  })
})

OR put the vcr::use_cassette() block on the inside, but put testthat expectations outside of the vcr::use_cassette() block:

library(testthat)
test_that("my test", {
  vcr::use_cassette("rl_citation", {
    aa <- rl_citation()
  })
 
  expect_is(aa, "character")
  expect_match(aa, "IUCN")
  expect_match(aa, "www.iucnredlist.org")
})

Don't wrap the use_cassette() block inside your test_that() block with testthat expectations inside the use_cassette() block, as you'll only get the line number that the use_cassette() block starts on on failures.

  • When running tests or checks of your whole package, note that some users have found different results with devtools::check() vs. devtools::test(). It's not clear why this would make a difference. Do let us know if you run into this problem.

vcr in your R project

You can use vcr in an R project as well.

  • Load vcr in your project
  • Similar to the above example, use use_cassette to run code that does HTTP requests.
  • The first time a real request is done, and after that the cached response will be used.

Installation

CRAN version:

install.packages("vcr")

Development version:

devtools::install_github("ropensci/vcr")
library("vcr")
library("crul")

Configuration

We set the following defaults:

  • dir = "."
  • record = "once"
  • match_requests_on = c("method", "uri")
  • allow_unused_http_interactions = TRUE
  • serialize_with = "yaml"
  • persist_with = "FileSystem"
  • ignore_hosts = NULL
  • ignore_localhost = FALSE
  • ignore_request = NULL
  • uri_parser = crul::url_parse
  • preserve_exact_body_bytes = FALSE
  • turned_off = FALSE
  • ignore_cassettes = FALSE
  • re_record_interval = NULL
  • clean_outdated_http_interactions = NULL
  • cassettes = list()
  • linked_context = NULL
  • vcr_logging = "vcr.log"
  • vcr_logging_opts = list()

You can get the defaults programmatically with

vcr_config_defaults()

You can change all the above defaults with vcr_configure():

vcr_configure()

Calling vcr_configuration() gives you some of the more important defaults in a nice tidy print out

vcr_configuration()

Matching/Matchers

vcr looks for similarity in your HTTP requests to cached requests. You can set what is examined about the request with one or more of the following options:

  • body
  • headers
  • host
  • method
  • path
  • query
  • uri

By default, we use method (HTTP method, e.g., GET) and uri (test for exact match against URI, e.g., http://foo.com).

You can set your own options by tweaking the match_requests_on parameter:

use_cassette(name = "one", {
    cli$post("post", body = list(a = 5))
  },
  match_requests_on = c('method', 'headers', 'body')
)

vcr in other languages

The canonical vcr (in Ruby) lists ports in other languages at https://github.com/vcr/vcr

NOTE

There's a number of features in this package that are not yet supported, but for which their parameters are found in the package. For example, decode_compressed_response is a parameter in use_cassette() but it is ignored right now.

We've tried to make sure the parameters that are ignored are marked as such. Keep an eye out for package updates for changes in these parameters, and/or let us know you want it and we can move it up in the priority list.

Example packages using vcr

TODO

  • Logging
  • Provide toggling a re-record interval so you can say e.g., after 6 hrs, re-record a real response, updating the cached response
  • ...

Meta

  • Please report any issues or bugs
  • License: MIT
  • Get citation information for vcr in R doing citation(package = 'vcr')
  • Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

ropensci_footer

News

vcr 0.2.6

NEW FEATURES

  • gains function use_vcr() to setup vcr for your package. This requires 3 pkgs all in Suggests; so are not required if you don't need to use use_vcr() (#52) (#95) thanks @maelle for the feedback!
  • vcr actually supports all four recording modes: none, once, new_episodes, and all. once is what's used by default. See ?recording for description of the recording modes. For now the test file test-ause_cassette_record_modes.R gives some examples and what to expect for each record mode; in the future the http testing book will have much more information in the Record modes chapter https://ropensci.github.io/http-testing-book/record-modes.html (commit)

MINOR IMPROVEMENTS

  • lots of tidying for better/consistent style
  • fix for a partial argument call in as.list(): all to all.names (commit)

BUG FIXES

  • error thrown with httr due to wrong date format. the problem was in the webmockr package. see ropensci/webmockr#58 (#91) thanks @Bisaloo
  • fix for use_cassette() when using httr: we weren't collecting status_code and storing it with the cassette (#92) thanks @Bisaloo
  • fixes for use_cassette() for httr: was working fine with a single httr request, but not with 2 or more (#93) (#94) thanks @Rekyt
  • in error blocks with use_cassette() the URL is presented from the request, and if there's a secret (API key) in the URL as a query parameter (or in any other place in the URL) then that secret is shown to the world (including if the error block happens on CI on the public web). This is fixed now; we use directives from your filter_sensitive_data call in vcr_configure() to mask secrets in error messages (#89) (#90)

vcr 0.2.2

MINOR IMPROVEMENTS

  • typo fixes (#85) thanks @Rekyt
  • added to docs: at least one person has reported different results using vcr with devtools::check vs. devtools::test (#83)
  • changed suggested usage of vcr in test suites from use_cassette block wrapped in test_that to the other way around; leads to testthat pointing to the actual test line that failed rather than pointing to the start of the use_cassette block (#86)

BUG FIXES

  • Fix for %||% internal function. Was incorrectly doing logical comparison; when headers list was passed one or more of the tests in the if statement had length > 1. Dev R is testing for this (#87)

vcr 0.2.0

NEW FEATURES

  • gains support for the httr package. vcr now supports crul and httr. Some of the integration for httr is via webmockr, while some of the tooling resides here in vcr (#73) (#79)

BUG FIXES

  • fix handling of response bodies when not raw type (#77) (#78)

vcr 0.1.0

NEW FEATURES

  • released to CRAN

Reference manual

It appears you don't have a PDF plugin for this browser. You can click here to download the reference manual.