httptest
makes it easy for you to write tests that don’t
require a network connection. With capture_requests()
, you
can record responses from real requests so that you can use them later
in tests. A further benefit of testing with mocks is that you don’t have
to deal with authentication and authorization on the server in your
tests—you don’t need to supply real login credentials for your test
suite to run. You can have full test coverage of your code, both on
public continuous-integration services like Travis-CI and when you
submit packages to CRAN, all without having to publish secret tokens or
passwords.
It is important to ensure that the mocks you include in your test
suite do not inadvertently reveal private information as well. For many
requests and responses, the default behavior of
capture_requests
is to write out only the response body,
which makes for clean, easy-to-read test fixtures. For other responses,
however—those returning non-JSON content or an error status—it writes a
.R
file containing a httr
“response” object.
This response contains all of the headers and cookies that the server
returns, and if not addressed, it could publicly expose your personal
credentials.
httptest
provides a framework for sanitizing the
responses that capture_requests
records. By default, it
redacts the standard ways that auth credentials are passed in responses.
The framework is extensible and allows you to specify custom redaction
policies that match how your API accepts and returns sensitive
information. Common redacting functions are configurable and natural to
adapt to your needs, while the workflow also supports custom redacting
functions that can alter the recorded requests however you want,
including altering the response content and URL.
By default, the capture_requests
context evaluates the
redact_auth()
function on a response object before writing
it to disk. redact_auth
wraps more specific redacting
functions that do things like sanitize any cookies in the server
response. Note that request parameters that communicate your credentials
to the API, including cookies, authorization headers, basic HTTP auth,
and OAuth, are also purged from the recorded response file:
capture_requests()
only records the response, not the
request, even though an httr
response object generally
includes the request object.
What does “redacting” entail? We aren’t the CIA working with classified reports, taking a heavy black marker over certain details. In our case, redacting means replacing the sensitive content with the string “REDACTED”. Your recorded responses will be as “real” as possible: if, for example, you have an “Authorization” header in your request, the header will remain in your test fixture, but real token value will be replaced with “REDACTED”. And only the recorded responses will be affected—the actual response you’re capturing in your active R session is not modified, only the mock that is written out.
To illustrate, here’s a request that has a cookie in the response. Let’s record it.
capture_requests(simplify = FALSE, {
real_resp <- GET("http://httpbin.org/cookies/set?token=12345")
})
In the actual response object in our R session, the cookie is there:
real_resp$cookies
## domain flag path secure expiration name value
## 1 httpbin.org FALSE / FALSE <NA> token 12345
But when we load that recorded response in tests later, the cookie won’t appear because it was redacted:
mockfile <- "httpbin.org/cookies/set-5b2631.R"
mock <- source(mockfile)$value
mock$cookies
## domain flag path secure expiration name value
## 1 httpbin.org FALSE / FALSE <NA> token REDACTED
mock$all_headers[[1]][["set-cookie"]]
## [1] "REDACTED"
Side note: the example uses the
simplify=FALSE
option tocapture_requests
for illustration purposes. With the defaultsimplify=TRUE
, only the response body would be written to a mock file because this particular GET request returns JSON content. Thus, there would be no cookie present anyway.simplify=FALSE
forcescapture_requests
to write the verbose .R response object file for every request, not just those that don’t return JSON content.
Sensitive or personal information is not limited to response cookies or headers. Sometimes identifiers are built into URLs or response bodies. These may be less sensitive than auth tokens, but you may want to conceal or anonymize your data that is included in test fixtures.
Redacting functions can help with this content as well. You can use redactors on any part of the response object, not just the headers and cookies. A redactor is just a function that takes a response as input and returns a response object, so anything is possible if you write a custom redactor.
For example, in the API for Pivotal Tracker, the agile project management tool, the Pivotal project id is built into many of its URLs. As a result, it would appear in mock file paths you record. The id is also often included in the response body.
We’d rather not have that information leak in our test fixtures, so
in the pivotaltrackR
package, which wraps this API, we need to tell
capture_requests
to scrub this id when we record mocks.
To do this, we’ll use set_redactor()
to supply a custom
function. The project id is stored in the R session in
options(pivotal.project)
, so we can identify it and
find-and-replace it with gsub_response()
. The function
takes response
as its first argument and then passes the
rest to gsub()
, which is called on both the response URL
and the response body.
Note the formula shorthand: this follows the syntax in the purrr
package for
defining anonymous functions. It is equivalent to
function(response) gsub_response(response, getOption("pivotal.project"), "123")
.
Valid inputs to set_redactor()
include:
response
, and
returning a valid response
object.
as the “response” argumentNULL
, to override the default
redact_auth()
and do no redactingTo see this in action, let’s record a request:
Note that the actual project id appears in the data returned from the search.
However, the project id won’t be found in the recorded file. If we
load the recorded response in with_mock_api
, we’ll see the
value we substituted in:
Nor will the project id appear in the file path: since the redactor is evaluated before determining the file path to write to, if you alter the response URL, the destination file path will be generated based on the modified URL. In this case, our mock is written to “…/projects/123/stories-fb1776.json”, not “…/projects/my-project-name/stories-fb1776.json”.
We can do more response cleaning with custom functions. All of the
redactors in httptest
take the “response” object as their
first argument and return the response object modified in some way. This
lends them to pipelining, as with the magrittr
package.
Continuing with the pivotaltrackR
example, let’s also
prune the domain and API root path from the URLs we’re recording so that
we’re making shorter file paths:
If you’re writing a package that wraps an API and you need a custom
redactor to safely record API responses, you’ll want to make sure that
you always record with that redactor. You don’t want to forget
to call set_redactor()
in your R session and end up
recording fixtures that contain your auth secrets.
To make sure that your redactor is “always on” for your package,
httptest
enables you to define a package-level redactor. To
do this, put a redacting function in inst/httptest/redact.R
in your package. (In fact, the function in the above example is in inst/httptest/redact.R
in the pivotaltrackR
package.)
Any time you record requests while your package is loaded, as when
running tests or building vignettes, this function will be called on the
response
object before writing it to disk. It’s automatic:
set it there once and you never have to remember.
Finally, depending on how long the URLs are in the API requests you
make, you may need to programmatically shorten them if you’re planning
on submitting your package to CRAN because CRAN requires file names to
be 100 characters or less. Long file names throw a “non-portable file
paths” message in R CMD check
.
A good way to solve this problem is to use a request preprocessor: a
function that alters the content of your ‘httr’ request
before mapping it to a mock file. It’s like a redactor but for the
request object. Just as you can provide a custom function to modify
responses that are recorded, you can provide a function to tweak the
request being made in order to map the request to the right file in the
mocked context. Importantly, this lets you truncate the URLs, which then
map to files.
For example, if all of your API endpoints sit beneath
https://language.googleapis.com/v1/
, you could set a
request preprocessor like:
and then all mocked requests would look for a path starting with “api/” rather than “language.googleapis.com/v1/”, saving you (in this case) 23 characters.
You can also provide this function in
inst/httptest/request.R
, just as you can for the redactor,
and any time your package is loaded and you’re reading mock (previously
recorded) responses, this function will be called on the
request
object before mapping it to a file. For example, here
is the one from pivotaltrackR
: