vcr
is an R port of the Ruby gem VCR (i.e., a translation, there’s
no Ruby here :))
vcr
helps you stub and record HTTP requests so you don’t
have to repeat HTTP requests.
The main use case is for unit tests, but you can use it outside of the unit test use case.
vcr
works with the crul
, httr
and httr2
HTTP request packages.
Check out the HTTP
testing book for a lot more documentation on vcr
,
webmockr
, and crul
, and other packages.
vcr::use_vcr()
vcr::use_cassette()
to
save HTTP interactions to disk in “cassettes” filesNow your tests can work without any internet connection!
Demo of adding vcr testing to an R package, corresponding narrative.
CRAN
install.packages("vcr")
Development version
remotes::install_github("ropensci/vcr")
library("vcr")
The docs assume you are using testthat for your unit tests.
use_vcr
You can then set up your package to use vcr
with:
vcr::use_vcr()
This will:
vcr
into the DESCRIPTION
testthat
is setuptestthat
if nottests/fixtures
vcr
vcr
.gitattributes
file with settings for
vcr
./tests/testthat/helper-vcr.R
fileWhat you will see in the R console:
◉ Using package: vcr.example
◉ assuming fixtures at: tests/fixtures
✓ Adding vcr to Suggests field in DESCRIPTION
✓ Creating directory: ./tests/testthat
◉ Looking for testthat.R file or similar
✓ tests/testthat.R: added
✓ Adding vcr config to tests/testthat/helper-vcr.example.R
✓ Adding example test file tests/testthat/test-vcr_example.R
✓ .gitattributes: added
◉ Learn more about `vcr`: https://books.ropensci.org/http-testing/
Secrets often turn up in API work. A common example is an API key.
vcr
saves responses from APIs as YAML files, and this will
include your secrets unless you indicate to vcr
what they
are and how to protect them. The vcr_configure
function has
the filter_sensitive_data
argument function for just this
situation. The filter_sensitive_data
argument takes a named
list where the name of the list is the string that will be used
in the recorded cassettes instead of the secret, which is the
list item. vcr
will manage the replacement of that
for you, so all you need to do is to edit your helper-vcr.R
file like this:
library("vcr") # *Required* as vcr is set up on loading
invisible(vcr::vcr_configure(
dir = "../fixtures"
))
vcr::check_cassette_names()
Use the filter_sensitive_data
argument in the
vcr_configure
function to show vcr
how to keep
your secret. The best way to store secret information is to have it in a
.Renviron
file. Assuming that that is already in place,
supply a named list to the filter_sensitive_data
argument.
library("vcr")
invisible(vcr::vcr_configure(
filter_sensitive_data = list("<<<my_api_key>>>" = Sys.getenv('APIKEY')), # add this
dir = "../fixtures"
))
vcr::check_cassette_names()
Notice we wrote Sys.getenv('APIKEY')
and not the API key
directly, otherwise you’d have written your API key to a file that might
end up in a public repo.
The will get your secret information from the environment, and make
sure that whenever vcr
records a new cassette, it will
replace the secret information with
<<<my_api_key>>>
. You can find out more
about this in the HTTP testing book
chapter on security.
The addition of the line above will instruct vcr
to
replace any string in cassettes it records that are equivalent to your
string which is stored as the APIKEY
environmental variable
with the masking string <<<my_api_key>>>
.
In practice, you might get a YAML
that looks a little like
this:
http_interactions:
- request:
method: post
...
headers:
Accept: application/json, text/xml, application/xml, */*
Content-Type: application/json
api-key: <<<my_api_key>>>
...
Here, my APIKEY
environmental variable would have been
stored as the api-key
value, but vcr
has
realised this and recorded the string
<<<my_api_key>>>
instead.
Once the cassette is recorded, vcr
no longer needs the
API key as no real requests will be made. Furthermore, as by default
requests matching does not include the API key, things will work.
Now, how to ensure tests work in the absence of a real API key?
E.g. to have tests pass on continuous integration for external pull requests to your code repository.
tests/testthat/helper-vcr.R
)if (!nzchar(Sys.getenv("APIKEY"))) {
Sys.setenv("APIKEY" = "foobar")
}
.Renviron
A simple way to manage local environmental variables is to use an .Renviron
file. Your .Renviron
file might look like this:
APIKEY="mytotallysecretkey"
You can have this set at a project or user level, and
usethis
has the usethis::edit_r_environ()
function to help edit the file.
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.
The first time you run the tests, a “cassette” i.e. a file with
recorded HTTP interactions, is created at
tests/fixtures/rl_citation.yml
. The times after that, the
cassette will be used. If you change your code and more HTTP
interactions are needed in the code wrapped by
vcr::use_cassette("rl_citation"
, delete
tests/fixtures/rl_citation.yml
and run the tests again for
re-recording the cassette.
If you want to get a feel for how vcr works, although you don’t need too.
library(vcr)
library(crul)
cli <- crul::HttpClient$new(url = "https://eu.httpbin.org")
system.time(
use_cassette(name = "helloworld", {
cli$get("get")
})
)
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")
})
)
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
.
For tweaking things to your needs, make sure to read the docs about configuration (e.g., where are the fixtures saved? can they be re-recorded automatically regulary?) and request matching (how does vcr match a request to a recorded interaction?)
vcr_setup()
the default directory created to hold cassettes
is called fixtures/
as a signal as to what the folder
contains.See usage section
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.
You can use vcr
in any R project as well.
vcr
in your projectuse_cassette
to run
code that does HTTP requests.See also the configuration vignette.
We set the following defaults:
"."
"once"
"c("method", "uri")"
TRUE
"yaml"
FALSE
"FileSystem"
NULL
FALSE
NULL
"crul::url_parse"
FALSE
FALSE
NULL
FALSE
FALSE
list()
NULL
FALSE
list(file = "vcr.log", log_prefix = "Cassette", date = TRUE)
NULL
NULL
NULL
NULL
NULL
NULL
FALSE
TRUE
TRUE
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 configuration parameters in a nice tidy print out
vcr_configuration()
#> <vcr configuration>
#> Cassette Dir: new/path
#> Record: new_episodes
#> Serialize with: yaml
#> URI Parser: urltools::url_parse
#> Match Requests on: query, headers
#> Preserve Bytes?: TRUE
#> Logging?: FALSE
#> ignored hosts: google.com
#> ignore localhost?: TRUE
#> Write disk path:
For more details refer to the configuration vignette
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')
)
For more details refer to the request matching vignette.
There’s a number of features in this package that are not yet supported, but for which their parameters are found in the package.
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.