Skip to content

Commit

Permalink
Merge pull request #158 from surik/global-mock
Browse files Browse the repository at this point in the history
Use global mock in adapters
  • Loading branch information
parroty authored Oct 5, 2020
2 parents d0ffc47 + 0d4439d commit 9f7f520
Show file tree
Hide file tree
Showing 18 changed files with 201 additions and 35 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ It's inspired by Ruby's VCR (https://github.com/vcr/vcr), and trying to provide

### Notes
- ExVCR.Config functions must be called from setup or test. Calls outside of test process, such as in setup_all will not work.
- ExVCR implements global mock, which means that all HTTP client calls outside of `use_cassette` go through `meck.passthough/1`.

### Install
Add `:exvcr` to `deps` section of `mix.exs`.
Expand Down
25 changes: 25 additions & 0 deletions fixture/vcr_cassettes/hackney_get_localhost.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[
{
"request": {
"body": "",
"headers": [],
"method": "get",
"options": {
"with_body": "true"
},
"request_body": "",
"url": "http://localhost:34009/server"
},
"response": {
"binary": false,
"body": "test_response",
"headers": {
"server": "Cowboy",
"date": "Tue, 29 Sep 2020 11:50:14 GMT",
"content-length": "13"
},
"status_code": 200,
"type": "ok"
}
}
]
30 changes: 30 additions & 0 deletions fixture/vcr_cassettes/httpc_get_localhost.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[
{
"request": {
"body": "",
"headers": [],
"method": "get",
"options": {
"httpc_options": [],
"http_options": []
},
"request_body": "",
"url": "http://localhost:34010/server"
},
"response": {
"binary": false,
"body": "test_response",
"headers": {
"date": "Tue, 29 Sep 2020 11:52:26 GMT",
"server": "Cowboy",
"content-length": "13"
},
"status_code": [
"HTTP/1.1",
200,
"OK"
],
"type": "ok"
}
}
]
23 changes: 23 additions & 0 deletions fixture/vcr_cassettes/ibrowse_get_localhost.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{
"request": {
"body": "",
"headers": [],
"method": "get",
"options": [],
"request_body": "",
"url": "http://localhost:34011/server"
},
"response": {
"binary": false,
"body": "test_response",
"headers": {
"server": "Cowboy",
"date": "Tue, 29 Sep 2020 11:56:58 GMT",
"content-length": "13"
},
"status_code": 200,
"type": "ok"
}
}
]
13 changes: 13 additions & 0 deletions lib/exvcr/actor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,17 @@ defmodule ExVCR.Actor do
defcast set(x), do: new_state(x)
defcall get, state: state, do: reply(state)
end

defmodule CurrentRecorder do
@moduledoc """
Stores current recorder to be able to fetch it inside of the mocked version of the adapter.
"""

use ExActor.GenServer, export: __MODULE__

defstart(start_link(arg), do: initial_state(arg))

defcast(set(x), do: new_state(x))
defcall(get, state: state, do: reply(state))
end
end
4 changes: 2 additions & 2 deletions lib/exvcr/adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ defmodule ExVCR.Adapter do
@doc """
Returns list of the mock target methods with function name and callback.
"""
def target_methods(recorder), do: raise ExVCR.ImplementationMissingError
defoverridable [target_methods: 1]
def target_methods(), do: raise ExVCR.ImplementationMissingError
defoverridable [target_methods: 0]

@doc """
Generate key for searching response.
Expand Down
21 changes: 13 additions & 8 deletions lib/exvcr/adapter/hackney.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ defmodule ExVCR.Adapter.Hackney do
@doc """
Returns list of the mock target methods with function name and callback.
"""
def target_methods(recorder) do
def target_methods() do
[
{:request, &ExVCR.Recorder.request(recorder, [&1, &2, &3, &4, &5])},
{:request, &ExVCR.Recorder.request(recorder, [&1, &2, &3, &4])},
{:request, &ExVCR.Recorder.request(recorder, [&1, &2, &3])},
{:request, &ExVCR.Recorder.request(recorder, [&1, &2])},
{:request, &ExVCR.Recorder.request(recorder, [&1])},
{:body, &handle_body_request(recorder, [&1])},
{:body, &handle_body_request(recorder, [&1, &2])}
{:request, &ExVCR.Recorder.request([&1, &2, &3, &4, &5])},
{:request, &ExVCR.Recorder.request([&1, &2, &3, &4])},
{:request, &ExVCR.Recorder.request([&1, &2, &3])},
{:request, &ExVCR.Recorder.request([&1, &2])},
{:request, &ExVCR.Recorder.request([&1])},
{:body, &handle_body_request([&1])},
{:body, &handle_body_request([&1, &2])}
]
end

Expand Down Expand Up @@ -89,6 +89,11 @@ defmodule ExVCR.Adapter.Hackney do
end
end

defp handle_body_request(args) do
ExVCR.Actor.CurrentRecorder.get()
|> handle_body_request(args)
end

defp handle_body_request(recorder, [client]) do
handle_body_request(recorder, [client, :infinity])
end
Expand Down
6 changes: 3 additions & 3 deletions lib/exvcr/adapter/httpc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ defmodule ExVCR.Adapter.Httpc do
{:request, &ExVCR.Recorder.request(recorder, [&1,&2])}
{:request, &ExVCR.Recorder.request(recorder, [&1,&2,&3,&4,&5])}
"""
def target_methods(recorder) do
[ {:request, &ExVCR.Recorder.request(recorder, [&1])},
{:request, &ExVCR.Recorder.request(recorder, [&1,&2,&3,&4])} ]
def target_methods() do
[ {:request, &ExVCR.Recorder.request([&1])},
{:request, &ExVCR.Recorder.request([&1,&2,&3,&4])} ]
end

@doc """
Expand Down
10 changes: 5 additions & 5 deletions lib/exvcr/adapter/ibrowse.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ defmodule ExVCR.Adapter.IBrowse do
@doc """
Returns list of the mock target methods with function name and callback.
"""
def target_methods(recorder) do
[ {:send_req, &ExVCR.Recorder.request(recorder, [&1,&2,&3])},
{:send_req, &ExVCR.Recorder.request(recorder, [&1,&2,&3,&4])},
{:send_req, &ExVCR.Recorder.request(recorder, [&1,&2,&3,&4,&5])},
{:send_req, &ExVCR.Recorder.request(recorder, [&1,&2,&3,&4,&5,&6])} ]
def target_methods() do
[ {:send_req, &ExVCR.Recorder.request([&1,&2,&3])},
{:send_req, &ExVCR.Recorder.request([&1,&2,&3,&4])},
{:send_req, &ExVCR.Recorder.request([&1,&2,&3,&4,&5])},
{:send_req, &ExVCR.Recorder.request([&1,&2,&3,&4,&5,&6])} ]
end

@doc """
Expand Down
21 changes: 21 additions & 0 deletions lib/exvcr/application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule ExVCR.Application do
use Application

def start(_type, _args) do
for app <- [:hackney, :ibrowse, :httpc], true == Code.ensure_loaded?(app) do
app
|> target_methods()
|> Enum.each(fn {function, callback} ->
:meck.expect(app, function, callback)
end)
end

children = [ExVCR.Actor.CurrentRecorder]

Supervisor.start_link(children, strategy: :one_for_one, name: ExVCR.Supervisor)
end

defp target_methods(:hackney), do: ExVCR.Adapter.Hackney.target_methods()
defp target_methods(:ibrowse), do: ExVCR.Adapter.IBrowse.target_methods()
defp target_methods(:httpc), do: ExVCR.Adapter.Httpc.target_methods()
end
3 changes: 3 additions & 0 deletions lib/exvcr/handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ defmodule ExVCR.Handler do
@doc """
Get response from either server or cache.
"""
def get_response(nil, request) do
:meck.passthrough(request)
end
def get_response(recorder, request) do
if ignore_request?(request, recorder) do
get_response_from_server(request, recorder, false)
Expand Down
1 change: 0 additions & 1 deletion lib/exvcr/iex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ defmodule ExVCR.IEx do
ExVCR.Mock.mock_methods(recorder, unquote(adapter))
unquote(test)
after
:meck.unload(unquote(adapter).module_name)
ExVCR.MockLock.release_lock()
Recorder.get(recorder)
|> JSX.encode!
Expand Down
15 changes: 4 additions & 11 deletions lib/exvcr/mock.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ defmodule ExVCR.Mock do
[do: return_value] = unquote(test)
return_value
after
module_name = adapter_method().module_name
:meck.unload(module_name)
ExVCR.Actor.CurrentRecorder.set(nil)
ExVCR.MockLock.release_lock()
end
end
Expand Down Expand Up @@ -68,8 +67,7 @@ defmodule ExVCR.Mock do
after
recorder_result = Recorder.save(recorder)

module_name = adapter_method().module_name
:meck.unload(module_name)
ExVCR.Actor.CurrentRecorder.set(nil)
ExVCR.MockLock.release_lock()

recorder_result
Expand All @@ -89,19 +87,14 @@ defmodule ExVCR.Mock do
@doc """
Mock methods pre-defined for the specified adapter.
"""
def mock_methods(recorder, adapter) do
target_methods = adapter.target_methods(recorder)
module_name = adapter.module_name

def mock_methods(recorder, _adapter) do
parent_pid = self()
Task.async(fn ->
ExVCR.MockLock.ensure_started
ExVCR.MockLock.request_lock(self(), parent_pid)
receive do
:lock_granted ->
Enum.each(target_methods, fn({function, callback}) ->
:meck.expect(module_name, function, callback)
end)
ExVCR.Actor.CurrentRecorder.set(recorder)
end
end)
|> Task.await(:infinity)
Expand Down
5 changes: 3 additions & 2 deletions lib/exvcr/recorder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ defmodule ExVCR.Recorder do
Provides entry point to be called from :meck library. HTTP request arguments are specified as args parameter.
If response is not found in the cache, access to the server.
"""
def request(recorder, request) do
Handler.get_response(recorder, request)
def request(args) do
ExVCR.Actor.CurrentRecorder.get()
|> Handler.get_response(args)
end

@doc """
Expand Down
5 changes: 3 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ defmodule ExVCR.Mixfile do

# Configuration for the OTP application
def application do
[applications: [:meck, :exactor, :exjsx]]
[applications: [:meck, :exactor, :exjsx],
mod: {ExVCR.Application, []}]
end

# Returns the list of dependencies in the format:
Expand All @@ -25,7 +26,7 @@ defmodule ExVCR.Mixfile do
{:meck, "~> 0.8"},
{:exactor, "~> 2.2"},
{:exjsx, "~> 4.0"},
{:ibrowse, "~> 4.4", optional: true},
{:ibrowse, "4.4.0", optional: true},
{:httpotion, "~> 3.1", optional: true},
{:httpoison, "~> 1.0", optional: true},
{:excoveralls, "~> 0.8", only: :test},
Expand Down
16 changes: 16 additions & 0 deletions test/adapter_hackney_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,27 @@ defmodule ExVCR.Adapter.HackneyTest do
use ExUnit.Case, async: true
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney

@port 34009

setup_all do
HttpServer.start(path: "/server", port: @port, response: "test_response")
{:ok, _} = HTTPoison.start
on_exit fn ->
HttpServer.stop(@port)
end
:ok
end

test "passthrough works after cassette has been used" do
url = "http://localhost:#{@port}/server"
use_cassette "hackney_get_localhost" do
{:ok, status_code, _headers, _body} = :hackney.request(:get, url, [], [], [with_body: true])
assert status_code == 200
end
{:ok, status_code, _headers, _body} = :hackney.request(:get, url, [], [], [with_body: true])
assert status_code == 200
end

test "hackney request" do
use_cassette "hackney_get" do
{:ok, status_code, headers, client} = :hackney.request(:get, "http://www.example.com", [], [], [])
Expand Down
18 changes: 18 additions & 0 deletions test/adapter_httpc_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,29 @@ defmodule ExVCR.Adapter.HttpcTest do
use ExUnit.Case, async: true
use ExVCR.Mock, adapter: ExVCR.Adapter.Httpc

@port 34010

setup_all do
HttpServer.start(path: "/server", port: @port, response: "test_response")
Application.ensure_started(:inets)
on_exit fn ->
HttpServer.stop(@port)
end
:ok
end

test "passthrough works after cassette has been used" do
url = "http://localhost:#{@port}/server" |> to_char_list()
use_cassette "httpc_get_localhost" do
{:ok, result} = :httpc.request(url)
{{_http_version, status_code, _reason_phrase}, _headers, _body} = result
assert status_code == 200
end
{:ok, result} = :httpc.request(url)
{{_http_version, status_code, _reason_phrase}, _headers, _body} = result
assert status_code == 200
end

test "example httpc request/1" do
use_cassette "example_httpc_request_1" do
{:ok, result} = :httpc.request('http://example.com')
Expand Down
19 changes: 18 additions & 1 deletion test/adapter_ibrowse_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,29 @@ defmodule ExVCR.Adapter.IBrowseTest do
use ExUnit.Case, async: true
use ExVCR.Mock

@port 34011

setup_all do
HTTPotion.start
HttpServer.start(path: "/server", port: @port, response: "test_response")
Application.ensure_started(:ibrowse)
on_exit fn ->
HttpServer.stop(@port)
end
:ok
end


test "passthrough works after cassette has been used" do
url = "http://localhost:#{@port}/server" |> to_char_list()
use_cassette "ibrowse_get_localhost" do
{:ok, status_code, _headers, _body} = :ibrowse.send_req(url, [], :get)
assert status_code == '200'
end
{:ok, status_code, _headers, _body} = :ibrowse.send_req(url, [], :get)
assert status_code == '200'
end


test "example single request" do
use_cassette "example_ibrowse" do
{:ok, status_code, headers, body} = :ibrowse.send_req('http://example.com', [], :get)
Expand Down

0 comments on commit 9f7f520

Please sign in to comment.