Skip to content

Commit e1e1f13

Browse files
authored
Support for mocked client requests. (#41)
1 parent 1591f3f commit e1e1f13

File tree

6 files changed

+246
-0
lines changed

6 files changed

+246
-0
lines changed

guides/links.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mocking:
2+
order: 5

guides/mocking/readme.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Mocking
2+
3+
This guide explains how to modify `Async::HTTP::Client` for mocking responses in tests.
4+
5+
## Mocking HTTP Responses
6+
7+
The mocking feature of `Async::HTTP` uses a real server running in a separate task, and routes all requests to it. This allows you to intercept requests and return custom responses, but still use the real HTTP client.
8+
9+
In order to enable this feature, you must create an instance of {ruby Async::HTTP::Mock::Endpoint} which will handle the requests.
10+
11+
~~~ ruby
12+
require 'async/http'
13+
require 'async/http/mock'
14+
15+
mock_endpoint = Async::HTTP::Mock::Endpoint.new
16+
17+
Sync do
18+
# Start a background server:
19+
server_task = Async(transient: true) do
20+
mock_endpoint.run do |request|
21+
# Respond to the request:
22+
::Protocol::HTTP::Response[200, {}, ["Hello, World"]]
23+
end
24+
end
25+
26+
endpoint = Async::HTTP::Endpoint.parse("https://www.google.com")
27+
mocked_endpoint = mock_endpoint.wrap(endpoint)
28+
client = Async::HTTP::Client.new(mocked_endpoint)
29+
30+
response = client.get("/")
31+
puts response.read
32+
# => "Hello, World"
33+
end
34+
~~~
35+
36+
## Transparent Mocking
37+
38+
Using your test framework's mocking capabilities, you can easily replace the `Async::HTTP::Client#new` with a method that returns a client with a mocked endpoint.
39+
40+
### Sus Integration
41+
42+
~~~ ruby
43+
require 'async/http'
44+
require 'async/http/mock'
45+
require 'sus/fixtures/async/reactor_context'
46+
47+
include Sus::Fixtures::Async::ReactorContext
48+
49+
let(:mock_endpoint) {Async::HTTP::Mock::Endpoint.new}
50+
51+
def before
52+
super
53+
54+
# Mock the HTTP client:
55+
mock(Async::HTTP::Client) do |mock|
56+
mock.wrap(:new) do |original, endpoint|
57+
original.call(mock_endpoint.wrap(endpoint))
58+
end
59+
end
60+
61+
# Run the mock server:
62+
Async(transient: true) do
63+
mock_endpoint.run do |request|
64+
::Protocol::HTTP::Response[200, {}, ["Hello, World"]]
65+
end
66+
end
67+
end
68+
69+
it "should perform a web request" do
70+
client = Async::HTTP::Client.new(Async::HTTP::Endpoint.parse("https://www.google.com"))
71+
response = client.get("/")
72+
# The response is mocked:
73+
expect(response.read).to be == "Hello, World"
74+
end
75+
~~~

lib/async/http.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@
88
require_relative 'http/client'
99
require_relative 'http/server'
1010

11+
require_relative 'http/internet'
12+
1113
require_relative 'http/endpoint'

lib/async/http/mock.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
#
3+
# Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in
13+
# all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
# THE SOFTWARE.
22+
23+
require_relative 'mock/endpoint'

lib/async/http/mock/endpoint.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# frozen_string_literal: true
2+
#
3+
# Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in
13+
# all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
# THE SOFTWARE.
22+
23+
require_relative '../protocol'
24+
25+
require 'async/queue'
26+
27+
module Async
28+
module HTTP
29+
module Mock
30+
# This is an endpoint which bridges a client with a local server.
31+
class Endpoint
32+
def initialize(protocol = Protocol::HTTP2, scheme = "http", authority = "localhost", queue: Queue.new)
33+
@protocol = protocol
34+
@scheme = scheme
35+
@authority = authority
36+
37+
@queue = queue
38+
end
39+
40+
attr :protocol
41+
attr :scheme
42+
attr :authority
43+
44+
# Processing incoming connections
45+
# @yield [::HTTP::Protocol::Request] the requests as they come in.
46+
def run(parent: Task.current, &block)
47+
while peer = @queue.dequeue
48+
server = @protocol.server(peer)
49+
50+
parent.async do
51+
server.each(&block)
52+
end
53+
end
54+
end
55+
56+
def connect
57+
local, remote = ::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)
58+
59+
@queue.enqueue(remote)
60+
61+
return local
62+
end
63+
64+
def wrap(endpoint)
65+
self.class.new(@protocol, endpoint.scheme, endpoint.authority, queue: @queue)
66+
end
67+
end
68+
end
69+
end
70+
end

test/async/http/mock.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to deal
5+
# in the Software without restriction, including without limitation the rights
6+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
# copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
# THE SOFTWARE.
20+
21+
require 'async/http/mock'
22+
require 'async/http/endpoint'
23+
require 'async/http/client'
24+
25+
require 'sus/fixtures/async/reactor_context'
26+
27+
describe Async::HTTP::Mock do
28+
include Sus::Fixtures::Async::ReactorContext
29+
30+
let(:endpoint) {Async::HTTP::Mock::Endpoint.new}
31+
32+
it "can respond to requests" do
33+
server = Async do
34+
endpoint.run do |request|
35+
::Protocol::HTTP::Response[200, [], ["Hello World"]]
36+
end
37+
end
38+
39+
client = Async::HTTP::Client.new(endpoint)
40+
41+
response = client.get("/index")
42+
43+
expect(response).to be(:success?)
44+
expect(response.read).to be == "Hello World"
45+
end
46+
47+
with 'mocked client' do
48+
it "can mock a client" do
49+
server = Async do
50+
endpoint.run do |request|
51+
::Protocol::HTTP::Response[200, [], ["Authority: #{request.authority}"]]
52+
end
53+
end
54+
55+
mock(Async::HTTP::Client) do |mock|
56+
replacement_endpoint = self.endpoint
57+
58+
mock.wrap(:new) do |original, original_endpoint, **options|
59+
original.call(replacement_endpoint.wrap(original_endpoint), **options)
60+
end
61+
end
62+
63+
google_endpoint = Async::HTTP::Endpoint.parse("https://www.google.com")
64+
client = Async::HTTP::Client.new(google_endpoint)
65+
66+
response = client.get("/search?q=hello")
67+
68+
expect(response).to be(:success?)
69+
expect(response.read).to be == "Authority: www.google.com"
70+
ensure
71+
response&.close
72+
end
73+
end
74+
end

0 commit comments

Comments
 (0)