Skip to content

Commit

Permalink
Merge pull request #160 from surik/global-mock-flag
Browse files Browse the repository at this point in the history
Make global mock experimental feature and disable it by default
  • Loading branch information
parroty authored Oct 8, 2020
2 parents f518f13 + 982b6b7 commit 603a8e3
Show file tree
Hide file tree
Showing 11 changed files with 153 additions and 12 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ otp_release:
- 19.3
- 20.3

env:
- GLOBAL_MOCK=true
- GLOBAL_MOCK=false

script:
- "MIX_ENV=test mix do deps.get, compile, coveralls.travis"
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ It's inspired by Ruby's VCR (https://github.com/vcr/vcr), and trying to provide
- The JSON file can be recorded automatically (vcr_cassettes) or manually updated (custom_cassettes)

### 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`.

```elixir
def deps do
Expand Down Expand Up @@ -312,6 +311,39 @@ config :exvcr, [

If `exvcr` is defined as test-only dependency, describe the above statement in test-only config file (ex. `config\test.exs`) or make it conditional (ex. wrap with `if Mix.env == :test`).

### Global mock experimental feature

The global mock is an attempt to address a general issue with **exvcr being slow**, see [#107](https://github.com/parroty/exvcr/issues/107)

In general, every use_cassette takes around 500 ms so if you extensively use cassettes it could spend minutes doing `:meck.expect/2` and `:meck.unload/1`. Even `exvcr` tests need 40 seconds versus 1 second when global mock is used.

Since feature is **experimental** be careful when using it. Please note the following:

- ExVCR implements global mock, which means that all HTTP client calls outside of `use_cassette` go through `meck.passthough/1`.
- There are some report that the feature doesn't work in some case, see [the issue](https://github.com/parroty/exvcr/issues/159).
- By default, the global mocking disabled, to enabled it set the following in config:

```elixir
use Mix.Config

config :exvcr, [
global_mock: true
]
```

All tests that are written for `exvcr` could also be running in global mocking mode:

```
$ GLOBAL_MOCK=true mix test
.........................................................
Finished in 1.3 seconds
141 tests, 0 failures
Randomized with seed 905427
```

### Mix Tasks
The following tasks are added by including exvcr package.
- [mix vcr](#mix-vcr-show-cassettes)
Expand Down
3 changes: 3 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use Mix.Config

config :exvcr, [
global_mock: false,
vcr_cassette_library_dir: "fixture/vcr_cassettes",
custom_cassette_library_dir: "fixture/custom_cassettes",
filter_sensitive_data: [
Expand All @@ -13,3 +14,5 @@ config :exvcr, [
enable_global_settings: false,
strict_mode: false
]

if Mix.env == :test, do: import_config "test.exs"
5 changes: 5 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use Mix.Config

config :exvcr, [
global_mock: System.get_env("GLOBAL_MOCK") == "true"
]
7 changes: 7 additions & 0 deletions lib/exvcr/adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,17 @@ defmodule ExVCR.Adapter do

@doc """
Returns list of the mock target methods with function name and callback.
Implementation for global mock.
"""
def target_methods(), do: raise ExVCR.ImplementationMissingError
defoverridable [target_methods: 0]

@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]

@doc """
Generate key for searching response.
[url: url, method: method] needs to be returned.
Expand Down
20 changes: 20 additions & 0 deletions lib/exvcr/adapter/hackney.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ defmodule ExVCR.Adapter.Hackney do

@doc """
Returns list of the mock target methods with function name and callback.
Implementation for global mock.
"""
def target_methods() do
[
Expand All @@ -39,6 +40,21 @@ defmodule ExVCR.Adapter.Hackney do
]
end

@doc """
Returns list of the mock target methods with function name and callback.
"""
def target_methods(recorder) 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])}
]
end

@doc """
Generate key for searching response.
"""
Expand Down Expand Up @@ -94,6 +110,10 @@ defmodule ExVCR.Adapter.Hackney do
|> handle_body_request(args)
end

defp handle_body_request(nil, args) do
:meck.passthrough(args)
end

defp handle_body_request(recorder, [client]) do
handle_body_request(recorder, [client, :infinity])
end
Expand Down
13 changes: 13 additions & 0 deletions lib/exvcr/adapter/httpc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ defmodule ExVCR.Adapter.Httpc do

@doc """
Returns list of the mock target methods with function name and callback.
Implementation for global mock.
TODO:
{:request, &ExVCR.Recorder.request(recorder, [&1,&2])}
{:request, &ExVCR.Recorder.request(recorder, [&1,&2,&3,&4,&5])}
Expand All @@ -32,6 +33,18 @@ defmodule ExVCR.Adapter.Httpc do
{:request, &ExVCR.Recorder.request([&1,&2,&3,&4])} ]
end

@doc """
Returns list of the mock target methods with function name and callback.
TODO:
{: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])} ]
end


@doc """
Generate key for searching response.
"""
Expand Down
11 changes: 11 additions & 0 deletions lib/exvcr/adapter/ibrowse.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ defmodule ExVCR.Adapter.IBrowse do

@doc """
Returns list of the mock target methods with function name and callback.
Implementation for global mock.
"""
def target_methods() do
[ {:send_req, &ExVCR.Recorder.request([&1,&2,&3])},
Expand All @@ -31,6 +32,16 @@ defmodule ExVCR.Adapter.IBrowse do
{:send_req, &ExVCR.Recorder.request([&1,&2,&3,&4,&5,&6])} ]
end

@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])} ]
end

@doc """
Generate key for searching response.
"""
Expand Down
21 changes: 17 additions & 4 deletions lib/exvcr/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,33 @@ defmodule ExVCR.Application do
use Application

def start(_type, _args) do
children =
if global_mock_enabled?() do
globally_mock_adapters()
[ExVCR.Actor.CurrentRecorder]
else
[]
end

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

defp globally_mock_adapters 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()

def global_mock_enabled? do
Application.get_env(:exvcr, :global_mock, false)
end
end
32 changes: 28 additions & 4 deletions lib/exvcr/mock.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ defmodule ExVCR.Mock do
[do: return_value] = unquote(test)
return_value
after
ExVCR.Actor.CurrentRecorder.set(nil)
module_name = adapter_method().module_name
unload(module_name)
ExVCR.MockLock.release_lock()
end
end
Expand Down Expand Up @@ -67,7 +68,8 @@ defmodule ExVCR.Mock do
after
recorder_result = Recorder.save(recorder)

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

recorder_result
Expand All @@ -84,17 +86,39 @@ defmodule ExVCR.Mock do
end
end

@doc false
defp load(adapter, recorder) do
if ExVCR.Application.global_mock_enabled?() do
ExVCR.Actor.CurrentRecorder.set(recorder)
else
module_name = adapter.module_name
target_methods = adapter.target_methods(recorder)
Enum.each(target_methods, fn({function, callback}) ->
:meck.expect(module_name, function, callback)
end)
end
end

@doc false
def unload(module_name) do
if ExVCR.Application.global_mock_enabled?() do
ExVCR.Actor.CurrentRecorder.set(nil)
else
:meck.unload(module_name)
end
end

@doc """
Mock methods pre-defined for the specified adapter.
"""
def mock_methods(recorder, _adapter) do
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 ->
ExVCR.Actor.CurrentRecorder.set(recorder)
load(adapter, recorder)
end
end)
|> Task.await(:infinity)
Expand Down
13 changes: 11 additions & 2 deletions lib/exvcr/recorder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,19 @@ defmodule ExVCR.Recorder do
@doc """
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.
Implementation for global mock.
"""
def request(args) do
def request(request) do
ExVCR.Actor.CurrentRecorder.get()
|> Handler.get_response(args)
|> request(request)
end

@doc """
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)
end

@doc """
Expand Down

0 comments on commit 603a8e3

Please sign in to comment.