Giter Site home page Giter Site logo

ropensci / webmockr Goto Github PK

View Code? Open in Web Editor NEW
49.0 6.0 5.0 786 KB

R library for stubbing and setting expectations on HTTP requests

Home Page: https://docs.ropensci.org/webmockr

License: Other

R 98.13% Makefile 0.41% HTML 1.45%
http http-mocking rstats http-mock mock fakeweb tdd testing-tools testing r

webmockr's Introduction

webmockr

cran checks Project Status: Active - The project has reached a stable, usable state and is being actively developed. R-CMD-check codecov rstudio mirror downloads cran version

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 to hello%20world)
  • Query parameters
    • We normalize query parameter values so that URL encoded things match URL un-encoded things (e.g. message = hello world to message = hello%20world)
  • 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")
  • 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

Features

  • 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

Supported HTTP libraries

Install

from cran

install.packages("webmockr")

Dev version

remotes::install_github("ropensci/webmockr")
library(webmockr)

Enable webmockr

webmockr::enable()
#> CrulAdapter enabled!
#> HttrAdapter enabled!

Inside a test framework

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!")

Outside a test framework

library(crul)

Stubbed request based on uri only and with the default response

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/7.79.1 r-curl/5.0.0 crul/1.3
#>     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/7.79.1 r-curl/5.0.0 crul/1.3
#>     Accept-Encoding: gzip, deflate
#>     Accept: application/json, text/xml, application/xml, */*
#>   response_headers: 
#>   status: 418

Stubbing requests based on method, uri and query params

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/7.79.1 r-curl/5.0.0 crul/1.3
#>     Accept-Encoding: gzip, deflate
#>     Accept: application/json, text/xml, application/xml, */*
#>   response_headers: 
#>   status: 418

Stubbing requests and set expectation of a timeout

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.

Stubbing requests and set HTTP error expectation

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.

httr integration

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"

Writing to disk

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 both crul and httr

Many requests in a row

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.

Contributors

Meta

  • Please report any issues or bugs.
  • License: MIT
  • Get citation information for webmockr in R doing citation(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.

webmockr's People

Contributors

aaronwolen avatar bisaloo avatar jeroen avatar maelle avatar sckott avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

webmockr's Issues

Make request and stub registry print methods more consistent

right now we have, e.,g,:

request_registry()
#> <webmockr request registry> 
#>   Registered Requests  
#>   POST https://httpbin.org/post  with body {foo: bar}  with headers {Accept-Encoding: gzip, deflate, Accept: application/json, text/xml, application/xml, */*} was made 1 times

stub_registry()
#> <webmockr stub registry> 
#>  Registered Stubs
#>     post: https://httpbin.org/post  with body {"foo":"bar"}  with headers {"Accept-Encoding":"gzip, deflate","Accept":"application/json, text/xml, application/xml, */*"} 

things to do:

  • make case consistent for HTTP methods, e.g., post vs. POST
  • make quotes consistent. right now for request registry we already have the request as a character string which we then print, so quotes are gone. whereas with stub registry we have webmockr_stub_registry$request_stubs[[1]]$to_s() and somehow we leave quotes in. make them consistent (GONNA SKIP THIS FOR NOW)
  • make indentation consistent

then: specify order of returned things

e.g., in ruby webmock

stub_request(:get, "www.example.com").
  to_return({body: "abc"}).then.  #then() is just a syntactic sugar
  to_return({body: "def"}).then.
  to_raise(MyException)

Net::HTTP.get('www.example.com', '/')    # ===> "abc\n"
Net::HTTP.get('www.example.com', '/')    # ===> "def\n"
Net::HTTP.get('www.example.com', '/')    # ===> MyException raised

webmockr in tests without installing all adapters

👋 @sckott!

We're seeing a test failure over at HIBPwned, one of which is due to webmockr https://travis-ci.org/lockedata/HIBPwned/jobs/447952505#L1729 I'd like to enable webmockr for crul only. Would it be possible to pass an option? Or maybe could webmockr find which adapter to use automatically by e.g. looking at dependencies when called from a test context?

By the way it seems that the docs/comments of webmockr::enable() & of onLoad() aren't up-to-date

#' each adapter (currently only \pkg{crul}), as a result of running

## because it's the only http lib supported for now

Finish implementing config options

  • net_http_connect_on_start - ❌ REMOVE - not relevant to this pkg
  • show_stubbing_instructions- ✅ SUPPORT IT
  • query_values_notation - ❌ REMOVE - doesn't seem necessary
  • show_body_diff - ❌ REMOVE - seems useful, but don't have enough time to do it

or decide not to support and remove

vignettes

  • outside of test framework
  • inside of test framework

Support RFC6570 uri templates

e.g., in ruby

uri_template = Addressable::Template.new "www.example.com/{id}/"
stub_request(:any, uri_template)

Net::HTTP.get('www.example.com', '/webmock/')    # ===> Success

Error message with suggested stub code not giving bodies

library(webmockr)
library(crul)
x <- crul::HttpClient$new(url = "https://httpbin.org")
crul::mock()
x$post('post', body = list(foo = "bar"))
#> Error: Real HTTP connections are disabled.
#> Unregistered request: POST https://httpbin.org/post   with headers {Accept-Encoding: gzip, deflate, Accept: application/json, text/xml, application/xml, */*}
#> 
#> You can stub this request with the following snippet:
#> 
#>    stub_request('post', uri = 'https://httpbin.org/post') %>%
#>     wi_th(headers = list('Accept-Encoding' = 'gzip, deflate', 'Accept' = 'application/json, text/xml, application/xml, */*')) 

i.e,. no body given

curl support

I'm interested in using webmockr for a test suite of mine, but I'm currently using jeroen/curl for making http requests.

Are there plans on supporting curl or is this not happening anytime soon?

multiple intermediate headers on redirect

related Ruby webmock issue from 2014 bblimke/webmock#237

We can set a single set of response headers for a stub, but currently not for multiple sets of response headers - crul can now support all intermediate headers, along with httr - so ideally users would be able to stub the set of intermediate headers, not just the final headers returned

interesting that Ruby webmock didn't sort this out

Retries

Related to #4 (comment)

How would one test for the case when the API returns 429 say 3 times and then 200? How could I stub that in a test?

to_raise: method to set expectation for a specific exception

e.g., in ruby

require 'rest-client'
require 'webmock'
include WebMock::API
WebMock.enable!
stub_request(:any, 'www.example.net').to_raise(StandardError)
RestClient.post('www.example.net', 'abc') # ===> StandardError
#> StandardError: Exception from WebMock

Response header names retain case

When setting response headers to be returned in a stub request, the header names do not get changed to lowercase when received by crul. This is different from the normal behavior (in crul) for response headers from actual web requests, where all header names will be converted to lower case.

library(crul)
library(webmockr)
webmockr::enable()
stub <- webmockr::stub_request(uri = "http://httpbin.org/get") %>%
  webmockr::to_return(headers = list("Foo-Bar" = "baz"))
cli <- HttpClient$new(url = "http://httpbin.org/")
resp <- cli$get("get")
resp$response_headers
# $`Foo-Bar`
# [1] "baz"

I'm not sure whether this actually belongs here, or instead is an issue with crul? But since I believe the adapter for crul lives here, I thought I'd report it here first.

Make sure headers detected and included in stub statement accurately

Right now

x$get('get', query = list(foo = "bar"))
#> Error: Real HTTP connections are disabled.
#> Unregistered request: GET https://httpbin.org/get?foo=bar   with headers {User-Agent: libcurl/7.51.0 r-curl/2.6 crul/0.3.5.9981, Accept-Encoding: gzip, deflate}
#> 
#> You can stub this request with the following snippet:
#> 
#>    stub_request('get', url = 'https://httpbin.org/get?foo=bar')

Suggested stub should have the exact headers passed in the request

Type of response header value depends on how stub was constructed

For example, if a request stub is constructed with a decimal value for a response header, then the response header obtained from a request has a decimal as the value for the respective header. This may seem natural at first, however for real web requests all values are character vectors, not decimals. Hence, the behavior is inconsistent with real web requests, which kind of defeats the purpose of the package.

Example:

library(crul)
library(webmockr)
webmockr::enable()
stub <- webmockr::stub_request(uri = "http://httpbin.org/get") %>%
  webmockr::to_return(headers = list("foo-bar" = 10))
cli <- HttpClient$new(url = "http://httpbin.org/")
resp <- cli$get("get")
resp$response_headers
# $`foo-bar`
# [1] 10

webmockr::stub_registry_clear()
stub <- webmockr::stub_request(uri = "http://httpbin.org/get") %>%
  webmockr::to_return(headers = list("foo-bar" = "10"))
resp <- cli$get("get")
resp$response_headers
# $`foo-bar`
# [1] "10"

Comparison to httptest?

I'm wondering whether anyone's considered what this package provides in comparison to httptest? The approaches seem quite different, but the intent seems quite similar?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.