R library for stubbing and setting expectations on HTTP requests.
Port of the Ruby gem webmock
How it works in detail
The very very short version is: webmockr
helps you stub
HTTP requests so you don’t have to repeat yourself.
More details
You tell webmockr
what HTTP request you want to match
against and if it sees a request matching your criteria it doesn’t
actually do the HTTP request. Instead, it gives back the same object you
would have gotten back with a real request, but only with the bits it
knows about. For example, we can’t give back the actual data you’d get
from a real HTTP request as the request wasn’t performed.
In addition, if you set an expectation of what webmockr
should return, we return that. For example, if you expect a request to
return a 418 error (I’m a Teapot), then that’s what you’ll get.
What you can match against
- HTTP method (required)
Plus any single or combination of the following:
- URI
- Right now, we can match directly against URI’s, and with regex URI patterns. Eventually, we will support RFC 6570 URI templates.
- We normalize URI paths so that URL encoded things match URL
un-encoded things (e.g.Â
hello world
tohello world
)
- Query parameters
- We normalize query parameter values so that URL encoded things match
URL un-encoded things (e.g.Â
message = hello world
tomessage = hello world
)
- We normalize query parameter values so that URL encoded things match
URL un-encoded things (e.g.Â
- Request headers
- We normalize headers and treat all forms of same headers as equal.
For example, the following two sets of headers are equal:
list(H1 = "value1", content_length = 123, X_CuStOm_hEAder = "foo")
list(h1 = "value1", "Content-Length" = 123, "x-cuSTOM-HeAder" = "foo")
- We normalize headers and treat all forms of same headers as equal.
For example, the following two sets of headers are equal:
- Request body
Real HTTP requests
There’s a few scenarios to think about when using
webmockr
:
After doing
library(webmockr)
webmockr
is loaded but not turned on. At this point
webmockr
doesn’t change anythning.
Once you turn on webmockr
like
webmockr::enable()
webmockr
will now by default not allow real HTTP
requests from the http libraries that adapters are loaded for (right now
only crul
).
You can optionally allow real requests via
webmockr_allow_net_connect()
, and disallow real requests
via webmockr_disable_net_connect()
. You can check whether
you are allowing real requests with
webmockr_net_connect_allowed()
.
Certain kinds of real HTTP requests allowed: We don’t suppoprt this
yet, but you can allow localhost HTTP requests with the
allow_localhost
parameter in the
webmockr_configure()
function.
Storing actual HTTP responses
webmockr
doesn’t do that. Check out vcr
- Stubbing HTTP requests at low http client lib level
- Setting and verifying expectations on HTTP requests
- Matching requests based on method, URI, headers and body
- Support for
testthat
via vcr - Can be used for testing or outside of a testing context
- Supports async http request mocking with
crul
only
from cran
install.packages("webmockr")
Dev version
# install.packages("pak")
pak::pak("ropensci/webmockr")
library(webmockr)
webmockr::enable()
#> CrulAdapter enabled!
#> HttrAdapter enabled!
#> Httr2Adapter enabled!
library(crul)
library(testthat)
# make a stub
stub_request("get", "https://httpbin.org/get") %>%
to_return(body = "success!", status = 200)
#> <webmockr stub>
#> method: get
#> uri: https://httpbin.org/get
#> with:
#> query:
#> body:
#> request_headers:
#> to_return:
#> - status: 200
#> body: success!
#> response_headers:
#> should_timeout: FALSE
#> should_raise: FALSE
# check that it's in the stub registry
stub_registry()
#> <webmockr stub registry>
#> Registered Stubs
#> GET: https://httpbin.org/get | to_return: with body "success!" with status 200
# make the request
z <- crul::HttpClient$new(url = "https://httpbin.org")$get("get")
# run tests (nothing returned means it passed)
expect_is(z, "HttpResponse")
expect_equal(z$status_code, 200)
expect_equal(z$parse("UTF-8"), "success!")
library(crul)
stub_request("get", "https://httpbin.org/get")
#> <webmockr stub>
#> method: get
#> uri: https://httpbin.org/get
#> with:
#> query:
#> body:
#> request_headers:
#> to_return:
x <- HttpClient$new(url = "https://httpbin.org")
x$get('get')
#> <crul response>
#> url: https://httpbin.org/get
#> request_headers:
#> User-Agent: libcurl/8.6.0 r-curl/5.2.1 crul/1.5.0
#> Accept-Encoding: gzip, deflate
#> Accept: application/json, text/xml, application/xml, */*
#> response_headers:
#> status: 200
set return objects
stub_request("get", "https://httpbin.org/get") %>%
wi_th(
query = list(hello = "world")) %>%
to_return(status = 418)
#> <webmockr stub>
#> method: get
#> uri: https://httpbin.org/get
#> with:
#> query: hello=world
#> body:
#> request_headers:
#> to_return:
#> - status: 418
#> body:
#> response_headers:
#> should_timeout: FALSE
#> should_raise: FALSE
x$get('get', query = list(hello = "world"))
#> <crul response>
#> url: https://httpbin.org/get
#> request_headers:
#> User-Agent: libcurl/8.6.0 r-curl/5.2.1 crul/1.5.0
#> Accept-Encoding: gzip, deflate
#> Accept: application/json, text/xml, application/xml, */*
#> response_headers:
#> status: 418
stub_request("get", "https://httpbin.org/get") %>%
wi_th(query = list(hello = "world"),
headers = list('User-Agent' = 'libcurl/7.51.0 r-curl/2.6 crul/0.3.6',
'Accept-Encoding' = "gzip, deflate"))
#> <webmockr stub>
#> method: get
#> uri: https://httpbin.org/get
#> with:
#> query: hello=world
#> body:
#> request_headers: User-Agent=libcurl/7.51.0 r-cur..., Accept-Encoding=gzip, deflate
#> to_return:
stub_registry()
#> <webmockr stub registry>
#> Registered Stubs
#> GET: https://httpbin.org/get
#> GET: https://httpbin.org/get?hello=world | to_return: with status 418
#> GET: https://httpbin.org/get?hello=world with headers {"User-Agent":"libcurl/7.51.0 r-curl/2.6 crul/0.3.6","Accept-Encoding":"gzip, deflate"}
x <- HttpClient$new(url = "https://httpbin.org")
x$get('get', query = list(hello = "world"))
#> <crul response>
#> url: https://httpbin.org/get
#> request_headers:
#> User-Agent: libcurl/8.6.0 r-curl/5.2.1 crul/1.5.0
#> Accept-Encoding: gzip, deflate
#> Accept: application/json, text/xml, application/xml, */*
#> response_headers:
#> status: 418
stub_request("post", "https://httpbin.org/post") %>% to_timeout()
#> <webmockr stub>
#> method: post
#> uri: https://httpbin.org/post
#> with:
#> query:
#> body:
#> request_headers:
#> to_return:
#> - status:
#> body:
#> response_headers:
#> should_timeout: TRUE
#> should_raise: FALSE
x <- HttpClient$new(url = "https://httpbin.org")
x$post('post')
#> Error: Request Timeout (HTTP 408).
#> - The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time.
library(fauxpas)
stub_request("get", "https://httpbin.org/get?a=b") %>% to_raise(HTTPBadRequest)
#> <webmockr stub>
#> method: get
#> uri: https://httpbin.org/get?a=b
#> with:
#> query:
#> body:
#> request_headers:
#> to_return:
#> - status:
#> body:
#> response_headers:
#> should_timeout: FALSE
#> should_raise: HTTPBadRequest
x <- HttpClient$new(url = "https://httpbin.org")
x$get('get', query = list(a = "b"))
#> Error: Bad Request (HTTP 400).
#> - The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repeat the request without modifications.
library(webmockr)
library(httr)
#>
#> Attaching package: 'httr'
#> The following object is masked from 'package:crul':
#>
#> handle
# turn on httr mocking
httr_mock()
# no stub found
GET("https://httpbin.org/get")
#> Error: Real HTTP connections are disabled.
#> Unregistered request:
#> GET https://httpbin.org/get with headers {Accept: application/json, text/xml, application/xml, */*}
#>
#> You can stub this request with the following snippet:
#>
#> stub_request('get', uri = 'https://httpbin.org/get') %>%
#> wi_th(
#> headers = list('Accept' = 'application/json, text/xml, application/xml, */*')
#> )
#> ============================================================
make a stub
stub_request('get', uri = 'https://httpbin.org/get') %>%
wi_th(
headers = list('Accept' = 'application/json, text/xml, application/xml, */*')
) %>%
to_return(status = 418, body = "I'm a teapot!!!", headers = list(im_a = "teapot"))
#> <webmockr stub>
#> method: get
#> uri: https://httpbin.org/get
#> with:
#> query:
#> body:
#> request_headers: Accept=application/json, te...
#> to_return:
#> - status: 418
#> body: I'm a teapot!!!
#> response_headers: im_a=teapot
#> should_timeout: FALSE
#> should_raise: FALSE
now returns mocked response
(res <- GET("https://httpbin.org/get"))
res$status_code
#> [1] 418
res$headers
#> $im_a
#> [1] "teapot"
library(webmockr)
library(httr2)
# turn on httr2 mocking
enable()
# no stub found
req <- request("https://hb.opencpu.org/get")
req_perform(req)
#> Error: Real HTTP connections are disabled.
#> Unregistered request:
#> GET https://hb.opencpu.org/get
#>
#> You can stub this request with the following snippet:
#>
#> stub_request('get', uri = 'https://hb.opencpu.org/get')
#> ============================================================
make a stub
stub_request('get', uri = 'https://hb.opencpu.org/get') %>%
to_return(status = 418, body = "I'm a teapot!!!", headers = list(im_a = "teapot"))
#> <webmockr stub>
#> method: get
#> uri: https://hb.opencpu.org/get
#> with:
#> query:
#> body:
#> request_headers:
#> to_return:
#> - status: 418
#> body: I'm a teapot!!!
#> response_headers: im_a=teapot
#> should_timeout: FALSE
#> should_raise: FALSE
now returns mocked response
req <- request("https://hb.opencpu.org/get")
res <- req_perform(req)
res
res$status_code
#> [1] 418
res$headers
#> <httr2_headers/list>
#> im_a: teapot
Write to a file before mocked request
## make a temp file
f <- tempfile(fileext = ".json")
## write something to the file
cat("{\"hello\":\"world\"}\n", file = f)
readLines(f)
#> [1] "{\"hello\":\"world\"}"
## make the stub
invisible(stub_request("get", "https://httpbin.org/get") %>%
to_return(body = file(f)))
## make a request
out <- HttpClient$new("https://httpbin.org/get")$get(disk = f)
readLines(file(f))
#> [1] "{\"hello\":\"world\"}"
OR - you can use mock_file()
to have webmockr
handle file and contents
g <- tempfile(fileext = ".json")
## make the stub
invisible(stub_request("get", "https://httpbin.org/get") %>%
to_return(body = mock_file(g, "{\"hello\":\"mars\"}\n")))
## make a request
out <- crul::HttpClient$new("https://httpbin.org/get")$get(disk = g)
readLines(out$content)
#> [1] "{\"hello\":\"world\"}"
Writing to disk is supported in crul
, httr
, and httr2
e.g., many redirects, then a final successful request
webmockr::enable()
library(crul)
library(fauxpas)
z <- stub_request("get", "https://httpbin.org/get")
to_return(z, status = 200, body = "foobar", headers = list(a = 5))
to_return(z, status = 200, body = "bears", headers = list(b = 6))
to_raise(z, HTTPBadRequest)
z
con <- crul::HttpClient$new(url = "https://httpbin.org")
# the first to_return()
first <- con$get("get")
first
first$parse("UTF-8")
# the second to_return()
second <- con$get("get")
second
second$parse("UTF-8")
# the third to_return() - fails as specified
third <- con$get("get")
Note that subsequent requests past the number of responses given with to_return()
/etc.
simply gives the last response you specified. Although if you set a to_timeout
or
to_raise
this feature won't happen since you fail out.
- Please report any issues or bugs.
- License: MIT
- Get citation information for
webmockr
in R doingcitation(package = 'webmockr')
- Please note that this package is released with a Contributor Code of Conduct. By contributing to this project, you agree to abide by its terms.