WebMock
=======
Library for stubbing and setting expectations on HTTP requests in Ruby.
Features
--------
* Stubbing HTTP requests at low http client lib level (no need to change tests when you change HTTP library)
* Setting and verifying expectations on HTTP requests
* Matching requests based on method, URI, headers and body
* Smart matching of the same URIs in different representations (also encoded and non encoded forms)
* Smart matching of the same headers in different representations.
* Support for Test::Unit
* Support for RSpec 1.x and RSpec 2.x
Supported HTTP libraries
------------------------
* Net::HTTP and libraries based on Net::HTTP (i.e RightHttpConnection, REST Client, HTTParty)
* HTTPClient
* Patron
* EM-HTTP-Request
* Curb
##Installation
gem install webmock --source http://gemcutter.org
### Test::Unit
Add the following code to `test/test_helper.rb`
require 'webmock/test_unit'
class Test::Unit::TestCase
include WebMock::API
end
### RSpec
Add the following code to `spec/spec_helper`:
require 'webmock/rspec'
Spec::Runner.configure do |config|
config.include WebMock::API
end
### RSpec 2
Add the following code to `spec/spec_helper`:
require 'webmock/rspec'
RSpec.configure do |config|
config.include WebMock::API
end
### Cucumber
Add the following code to `features/support/env.rb`
require 'webmock/rspec'
World(WebMock::API, WebMock::Matchers)
You can also use WebMock outside a test framework:
require 'webmock'
include WebMock::API
include WebMock::Matchers
## Examples
## Stubbing
### Stubbed request based on uri only and with the default response
stub_request(:any, "www.example.com")
Net::HTTP.get("www.example.com", "/") # ===> Success
### Stubbing requests based on method, uri, body and headers
stub_request(:post, "www.example.com").with(:body => "abc", :headers => { 'Content-Length' => 3 })
uri = URI.parse("http://www.example.com/")
req = Net::HTTP::Post.new(uri.path)
req['Content-Length'] = 3
res = Net::HTTP.start(uri.host, uri.port) {|http|
http.request(req, "abc")
} # ===> Success
### Matching request body and headers against regular expressions
stub_request(:post, "www.example.com").
with(:body => /^.*world$/, :headers => {"Content-Type" => /image\/.+/}).to_return(:body => "abc")
uri = URI.parse('http://www.example.com/')
req = Net::HTTP::Post.new(uri.path)
req['Content-Type'] = 'image/png'
res = Net::HTTP.start(uri.host, uri.port) {|http|
http.request(req, 'hello world')
} # ===> Success
### Matching request body against a hash. Body can be URL-Encoded, JSON or XML.
stub_http_request(:post, "www.example.com").
with(:body => {:data => {:a => '1', :b => 'five'}})
RestClient.post('www.example.com', "data[a]=1&data[b]=five",
:content_type => 'application/x-www-form-urlencoded') # ===> Success
RestClient.post('www.example.com', '{"data":{"a":"1","b":"five"}}',
:content_type => 'application/json') # ===> Success
RestClient.post('www.example.com', '',
:content_type => 'application/xml' ) # ===> Success
### Matching custom request headers
stub_request(:any, "www.example.com").with(:headers=>{ 'Header-Name' => 'Header-Value' })
uri = URI.parse('http://www.example.com/')
req = Net::HTTP::Post.new(uri.path)
req['Header-Name'] = 'Header-Value'
res = Net::HTTP.start(uri.host, uri.port) {|http|
http.request(req, 'abc')
} # ===> Success
### Matching multiple headers with the same name
stub_http_request(:get, 'www.example.com').with(:headers => {'Accept' => ['image/jpeg', 'image/png'] })
req = Net::HTTP::Get.new("/")
req['Accept'] = ['image/png']
req.add_field('Accept', 'image/jpeg')
Net::HTTP.start("www.example.com") {|http| http.request(req) } # ===> Success
### Matching requests against provided block
stub_request(:post, "www.example.com").with { |request| request.body == "abc" }
RestClient.post('www.example.com', 'abc') # ===> Success
### Request with basic authentication
stub_request(:get, "user:pass@www.example.com")
Net::HTTP.start('www.example.com') {|http|
req = Net::HTTP::Get.new('/')
req.basic_auth 'user', 'pass'
http.request(req)
} # ===> Success
### Matching uris using regular expressions
stub_request(:any, /.*example.*/)
Net::HTTP.get('www.example.com', '/') # ===> Success
### Matching query params using hash
stub_http_request(:get, "www.example.com").with(:query => {"a" => ["b", "c"]})
RestClient.get("http://www.example.com/?a[]=b&a[]=c") # ===> Success
### Stubbing with custom response
stub_request(:any, "www.example.com").to_return(:body => "abc", :status => 200, :headers => { 'Content-Length' => 3 } )
Net::HTTP.get("www.example.com", '/') # ===> "abc"
### Response with body specified as IO object
File.open('/tmp/response_body.txt', 'w') { |f| f.puts 'abc' }
stub_request(:any, "www.example.com").to_return(:body => File.new('/tmp/response_body.txt'), :status => 200)
Net::HTTP.get('www.example.com', '/') # ===> "abc\n"
### Response with custom status message
stub_request(:any, "www.example.com").to_return(:status => [500, "Internal Server Error"])
req = Net::HTTP::Get.new("/")
Net::HTTP.start("www.example.com") { |http| http.request(req) }.message # ===> "Internal Server Error"
### Replaying raw responses recorded with `curl -is`
`curl -is www.example.com > /tmp/example_curl_-is_output.txt`
raw_response_file = File.new("/tmp/example_curl_-is_output.txt")
from file
stub_request(:get, "www.example.com").to_return(raw_response_file)
or string
stub_request(:get, "www.example.com").to_return(raw_response_file.read)
### Responses dynamically evaluated from block
stub_request(:any, 'www.example.net').
to_return { |request| {:body => request.body} }
RestClient.post('www.example.net', 'abc') # ===> "abc\n"
### Responses dynamically evaluated from lambda
stub_request(:any, 'www.example.net').
to_return(lambda { |request| {:body => request.body} })
RestClient.post('www.example.net', 'abc') # ===> "abc\n"
### Dynamically evaluated raw responses recorded with `curl -is`
`curl -is www.example.com > /tmp/www.example.com.txt`
stub_request(:get, "www.example.com").to_return(lambda { |request| File.new("/tmp/#{request.uri.host.to_s}.txt" }))
### Responses with dynamically evaluated parts
stub_request(:any, 'www.example.net').
to_return(:body => lambda { |request| request.body })
RestClient.post('www.example.net', 'abc') # ===> "abc\n"
### Raising errors
#### Exception declared by class
stub_request(:any, 'www.example.net').to_raise(StandardError)
RestClient.post('www.example.net', 'abc') # ===> StandardError
#### or by exception instance
stub_request(:any, 'www.example.net').to_raise(StandardError.new("some error"))
#### or by string
stub_request(:any, 'www.example.net').to_raise("some error")
### Raising timeout errors
stub_request(:any, 'www.example.net').to_timeout
RestClient.post('www.example.net', 'abc') # ===> RestClient::RequestTimeout
### Multiple responses for repeated requests
stub_request(:get, "www.example.com").to_return({:body => "abc"}, {:body => "def"})
Net::HTTP.get('www.example.com', '/') # ===> "abc\n"
Net::HTTP.get('www.example.com', '/') # ===> "def\n"
#after all responses are used the last response will be returned infinitely
Net::HTTP.get('www.example.com', '/') # ===> "def\n"
### Multiple responses using chained `to_return()`, `to_raise()` or `to_timeout` declarations
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
### Specifying number of times given response should be returned
stub_request(:get, "www.example.com").
to_return({:body => "abc"}).times(2).then.
to_return({:body => "def"})
Net::HTTP.get('www.example.com', '/') # ===> "abc\n"
Net::HTTP.get('www.example.com', '/') # ===> "abc\n"
Net::HTTP.get('www.example.com', '/') # ===> "def\n"
### Real requests to network can be allowed or disabled
WebMock.allow_net_connect!
stub_request(:any, "www.example.com").to_return(:body => "abc")
Net::HTTP.get('www.example.com', '/') # ===> "abc"
Net::HTTP.get('www.something.com', '/') # ===> /.+Something.+/
WebMock.disable_net_connect!
Net::HTTP.get('www.something.com', '/') # ===> Failure
### External requests can be disabled while allowing localhost
WebMock.disable_net_connect!(:allow_localhost => true)
Net::HTTP.get('www.something.com', '/') # ===> Failure
Net::HTTP.get('localhost:9887', '/') # ===> Allowed. Perhaps to Selenium?
### External requests can be disabled while allowing any hostname
WebMock.disable_net_connect!(:allow => "www.example.org")
Net::HTTP.get('www.something.com', '/') # ===> Failure
Net::HTTP.get('www.example.org', '/') # ===> Allowed.
## Connecting on Net::HTTP.start
HTTP protocol has 3 steps: connect, request and response (or 4 with close). Most Ruby HTTP client libraries
treat connect as a part of request step, with the exception of `Net::HTTP` which
allows opening connection to the server separately to the request, by using `Net::HTTP.start`.
WebMock API was also designed with connect being part of request step, and it only allows stubbing
requests, not connections. When `Net::HTTP.start` is called, WebMock doesn't know yet whether
a request is stubbed or not. WebMock by default delays a connection until the request is invoked,
so when there is no request, `Net::HTTP.start` doesn't do anything.
**This means that WebMock breaks the Net::HTTP behaviour by default!**
To workaround this issue, WebMock offers `:net_http_connect_on_start` option,
which can be passed to `WebMock.allow_net_connect!` and `WebMock#disable_net_connect!` methods, i.e.
WebMock.allow_net_connect!(:net_http_connect_on_start => true)
This forces WebMock Net::HTTP adapter to always connect on `Net::HTTP.start`.
## Setting Expectations
### Setting expectations in Test::Unit
require 'webmock/test_unit'
stub_request(:any, "www.example.com")
uri = URI.parse('http://www.example.com/')
req = Net::HTTP::Post.new(uri.path)
req['Content-Length'] = 3
res = Net::HTTP.start(uri.host, uri.port) {|http|
http.request(req, 'abc')
}
assert_requested :post, "http://www.example.com",
:headers => {'Content-Length' => 3}, :body => "abc", :times => 1 # ===> Success
assert_not_requested :get, "http://www.something.com" # ===> Success
assert_requested(:post, "http://www.example.com", :times => 1) { |req| req.body == "abc" }
### Expecting real (not stubbed) requests
WebMock.allow_net_connect!
Net::HTTP.get('www.example.com', '/') # ===> Success
assert_requested :get, "http://www.example.com" # ===> Success
### Setting expectations in RSpec
This style is borrowed from [fakeweb-matcher](http://github.com/freelancing-god/fakeweb-matcher)
require 'webmock/rspec'
WebMock.should have_requested(:get, "www.example.com").with(:body => "abc", :headers => {'Content-Length' => 3}).twice
WebMock.should_not have_requested(:get, "www.something.com")
WebMock.should have_requested(:post, "www.example.com").with { |req| req.body == "abc" }
WebMock.should have_requested(:get, "www.example.com").with(:query => {"a" => ["b", "c"]})
WebMock.should have_requested(:get, "www.example.com").
with(:body => {"a" => ["b", "c"]}, :headers => 'Content-Type' => 'application/json')
### Different way of setting expectations in RSpec
a_request(:post, "www.example.com").with(:body => "abc", :headers => {'Content-Length' => 3}).should have_been_made.once
a_request(:post, "www.something.com").should have_been_made.times(3)
a_request(:any, "www.example.com").should_not have_been_made
a_request(:post, "www.example.com").with { |req| req.body == "abc" }.should have_been_made
a_request(:get, "www.example.com").with(:query => {"a" => ["b", "c"]}).should have_been_made
a_request(:post, "www.example.com").
with(:body => {"a" => ["b", "c"]}, :headers => 'Content-Type' => 'application/json').should have_been_made
## Clearing stubs and request history
If you want to reset all current stubs and history of requests use `WebMock.reset_webmock`
stub_request(:any, "www.example.com")
Net::HTTP.get('www.example.com', '/') # ===> Success
reset_webmock
Net::HTTP.get('www.example.com', '/') # ===> Failure
assert_not_requested :get, "www.example.com" # ===> Success
## Matching requests
An executed request matches stubbed request if it passes following criteria:
When request URI matches stubbed request URI string or Regexp pattern
And request method is the same as stubbed request method or stubbed request method is :any
And request body is the same as stubbed request body or stubbed request body is not specified
And request headers match stubbed request headers, or stubbed request headers match a subset of request headers, or stubbed request headers are not specified
And request matches provided block or block is not provided
## Precedence of stubs
Always the last declared stub matching the request will be applied i.e:
stub_request(:get, "www.example.com").to_return(:body => "abc")
stub_request(:get, "www.example.com").to_return(:body => "def")
Net::HTTP.get('www.example.com', '/') # ====> "def"
## Matching URIs
WebMock will match all different representations of the same URI.
I.e all the following representations of the URI are equal:
"www.example.com"
"www.example.com/"
"www.example.com:80"
"www.example.com:80/"
"http://www.example.com"
"http://www.example.com/"
"http://www.example.com:80"
"http://www.example.com:80/"
The following URIs with basic authentication are also equal for WebMock
"a b:pass@www.example.com"
"a b:pass@www.example.com/"
"a b:pass@www.example.com:80"
"a b:pass@www.example.com:80/"
"http://a b:pass@www.example.com"
"http://a b:pass@www.example.com/"
"http://a b:pass@www.example.com:80"
"http://a b:pass@www.example.com:80/"
"a%20b:pass@www.example.com"
"a%20b:pass@www.example.com/"
"a%20b:pass@www.example.com:80"
"a%20b:pass@www.example.com:80/"
"http://a%20b:pass@www.example.com"
"http://a%20b:pass@www.example.com/"
"http://a%20b:pass@www.example.com:80"
"http://a%20b:pass@www.example.com:80/"
or these
"www.example.com/my path/?a=my param&b=c"
"www.example.com/my%20path/?a=my%20param&b=c"
"www.example.com:80/my path/?a=my param&b=c"
"www.example.com:80/my%20path/?a=my%20param&b=c"
"http://www.example.com/my path/?a=my param&b=c"
"http://www.example.com/my%20path/?a=my%20param&b=c"
"http://www.example.com:80/my path/?a=my param&b=c"
"http://www.example.com:80/my%20path/?a=my%20param&b=c"
If you provide Regexp to match URI, WebMock will try to match it against every valid form of the same url.
I.e `/.*my param.*/` will match `www.example.com/my%20path` because it is equivalent of `www.example.com/my path`
## Matching headers
WebMock will match request headers against stubbed request headers in the following situations:
1. Stubbed request has headers specified and request headers are the same as stubbed headers
i.e stubbed headers: `{ 'Header1' => 'Value1', 'Header1' => 'Value1' }`, requested: `{ 'Header1' => 'Value1', 'Header1' => 'Value1' }`
2. Stubbed request has headers specified and stubbed request headers are a subset of request headers
i.e stubbed headers: `{ 'Header1' => 'Value1' }`, requested: `{ 'Header1' => 'Value1', 'Header1' => 'Value1' }`
3. Stubbed request has no headers
i.e stubbed headers: `nil`, requested: `{ 'Header1' => 'Value1', 'Header1' => 'Value1' }`
WebMock normalises headers and treats all forms of same headers as equal:
i.e the following two sets of headers are equal:
`{ "Header1" => "value1", :content_length => 123, :X_CuStOm_hEAder => :value }`
`{ :header1 => "value1", "Content-Length" => 123, "x-cuSTOM-HeAder" => "value" }`
## Recording real requests and responses and replaying them later
To record your application's real HTTP interactions and replay them later in tests you can use [VCR](http://github.com/myronmarston/vcr) with WebMock.
## Request callbacks
####WebMock can invoke callbacks stubbed or real requests:
WebMock.after_request do |request_signature, response|
puts "Request #{request_signature} was made and #{response} was returned"
end
#### invoke callbacks for real requests only and except requests made with Patron
WebMock.after_request(:except => [:patron], :real_requests_only => true) do |request_signature, response|
puts "Request #{request_signature} was made and #{response} was returned"
end
## Bugs and Issues
Please submit them here [http://github.com/bblimke/webmock/issues](http://github.com/bblimke/webmock/issues)
## Suggestions
If you have any suggestions on how to improve WebMock please send an email to the mailing list [groups.google.com/group/webmock-users](http://groups.google.com/group/webmock-users)
I'm particularly interested in how the DSL could be improved.
## Credits
The initial lines of this project were written during New Bamboo [Hack Day](http://blog.new-bamboo.co.uk/2009/11/13/hackday-results)
Thanks to my fellow [Bambinos](http://new-bamboo.co.uk/) for all the great suggestions!
People who submitted patches and new features or suggested improvements. Many thanks to these people:
* Ben Pickles
* Mark Evans
* Ivan Vega
* Piotr Usewicz
* Nick Plante
* Nick Quaranto
* Diego E. "Flameeyes" Pettenò
* Niels Meersschaert
* Mack Earnhardt
* Arvicco
* Sergio Gil
* Jeffrey Jones
* Tekin Suleyman
* Tom Ward
* Nadim Bitar
* Myron Marston
* Sam Phillips
* Jose Angel Cortinas
* Razic
* Steve Tooke
* Nathaniel Bibler
* Martyn Loughran
* Muness Alrubaie
* Charles Li
* Ryan Bigg
* Pete Higgins
* Hans de Graaff
* Alastair Brunton
## Background
Thank you Fakeweb! This library was inspired by [FakeWeb](fakeweb.rubyforge.org).
I imported some solutions from that project to WebMock. I also copied some code i.e Net:HTTP adapter.
Fakeweb architecture unfortunately didn't allow me to extend it easily with the features I needed.
I also preferred some things to work differently i.e request stub precedence.
## Copyright
Copyright (c) 2009-2010 Bartosz Blimke. See LICENSE for details.