Exvcr
HTTP request/response recording library for elixir, inspired by VCR.
Install / Use
/learn @parroty/ExvcrREADME
ExVCR
Record and replay HTTP interactions library for Elixir. It's inspired by Ruby's VCR, and trying to provide similar functionalities.
Basics
The following HTTP libraries can be applied.
- ibrowse-based libraries.
- hackney-based libraries.
- HTTPoison
- support is very limited, and tested only with sync request of HTTPoison yet.
- httpc-based libraries.
- erlang-oauth
- tirexs
- support is very limited, and tested only with
:httpc.request/1and:httpc.request/4.
- Finch
- the deprecated
Finch.request/6functions is not supported
- the deprecated
HTTP interactions are recorded as JSON file. The JSON file can be recorded
automatically (vcr_cassettes) or manually updated (custom_cassettes).
Notes
ExVCR.Configfunctions must be called fromsetuportest. Calls outside of test process, such as insetup_allwill not work.
Install
Add :exvcr to deps section of mix.exs.
def deps do
[ {:exvcr, "~> 0.11", only: :test} ]
end
Optionally, preferred_cli_env: [vcr: :test] can be specified for running mix vcr in :test env by default.
def project do
[ ...
preferred_cli_env: [
vcr: :test, "vcr.delete": :test, "vcr.check": :test, "vcr.show": :test
],
...
end
Usage
Add use ExVCR.Mock to the test module. This mocks ibrowse by default. For
using hackney, specify adapter: ExVCR.Adapter.Hackney options as follows.
Example with ibrowse
defmodule ExVCR.Adapter.IBrowseTest do
use ExUnit.Case, async: true
use ExVCR.Mock
setup do
ExVCR.Config.cassette_library_dir("fixture/vcr_cassettes")
:ok
end
test "example single request" do
use_cassette "example_ibrowse" do
:ibrowse.start
{:ok, status_code, _headers, body} = :ibrowse.send_req('http://example.com', [], :get)
assert status_code == '200'
assert to_string(body) =~ ~r/Example Domain/
end
end
end
Example with hackney
defmodule ExVCR.Adapter.HackneyTest do
use ExUnit.Case, async: true
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
setup_all do
HTTPoison.start
:ok
end
test "get request" do
use_cassette "httpoison_get" do
assert HTTPoison.get!("http://example.com").body =~ ~r/Example Domain/
end
end
end
Example with httpc
defmodule ExVCR.Adapter.HttpcTest do
use ExUnit.Case, async: true
use ExVCR.Mock, adapter: ExVCR.Adapter.Httpc
setup_all do
:inets.start
:ok
end
test "get request" do
use_cassette "example_httpc_request" do
{:ok, result} = :httpc.request('http://example.com')
{{_http_version, _status_code = 200, _reason_phrase}, _headers, body} = result
assert to_string(body) =~ ~r/Example Domain/
end
end
end
Example with Finch
defmodule ExVCR.Adapter.FinchTest do
use ExUnit.Case, async: true
use ExVCR.Mock, adapter: ExVCR.Adapter.Finch
setup_all do
Finch.start_link(name: MyFinch)
:ok
end
test "get request" do
use_cassette "example_finch_request" do
{:ok, response} = Finch.build(:get, "http://example.com/") |> Finch.request(MyFinch)
assert response.status == 200
assert Map.new(response.headers)["content-type"] == "text/html; charset=UTF-8"
assert response.body =~ ~r/Example Domain/
end
end
end
Example with Start / Stop
Instead of single use_cassette, start_cassette and stop_cassette can serve as an alternative syntax.
use_cassette("x") do
do_something
end
start_cassette("x")
do_something
stop_cassette
Custom Cassettes
You can manually define custom cassette JSON file for more flexible response control rather than just recoding the actual server response.
-
Optional 2nd parameter of
ExVCR.Config.cassette_library_dirmethod specifies the custom cassette directory. The directory is separated from vcr cassette one for avoiding mistakenly overwriting. -
Adding
custom: trueoption touse_cassettemacro indicates to use the custom cassette, and it just returns the pre-defined JSON response, instead of requesting to server.
defmodule ExVCR.MockTest do
use ExUnit.Case, async: true
import ExVCR.Mock
setup do
ExVCR.Config.cassette_library_dir("fixture/vcr_cassettes", "fixture/custom_cassettes")
:ok
end
test "custom with valid response" do
use_cassette "response_mocking", custom: true do
assert HTTPoison.get!("http://example.com", []).body =~ ~r/Custom Response/
end
end
The custom JSON file format is the same as vcr cassettes.
fixture/custom_cassettes/response_mocking.json
[
{
"request": {
"url": "http://example.com"
},
"response": {
"status_code": 200,
"headers": {
"Content-Type": "text/html"
},
"body": "<h1>Custom Response</h1>"
}
}
]
Recording VCR Cassettes
Matching
ExVCR uses URL parameter to match request and cassettes. The url parameter in
the JSON file is taken as regexp string.
Removing Sensitive Data
ExVCR.Config.filter_sensitive_data(pattern, placeholder) method can be used
to remove sensitive data. It searches for string matches with pattern, which
is a string representing a regular expression, and replaces with placeholder.
Replacements happen both in URLs and request and response bodies.
test "replace sensitive data" do
ExVCR.Config.filter_sensitive_data("<PASSWORD>.+</PASSWORD>", "PLACEHOLDER")
use_cassette "sensitive_data" do
assert HTTPoison.get!("http://something.example.com", []).body =~ ~r/PLACEHOLDER/
end
end
ExVCR.Config.filter_request_headers(header) and
ExVCR.Config.filter_request_options(option) can be used to remove sensitive
data in the request headers. It checks if the header is found in the request
headers and blanks out it's value with ***.
test "replace sensitive data in request header" do
ExVCR.Config.filter_request_headers("X-My-Secret-Token")
use_cassette "sensitive_data_in_request_header" do
body = HTTPoison.get!("http://localhost:34000/server?", ["X-My-Secret-Token": "my-secret-token"]).body
assert body == "test_response"
end
# The recorded cassette should contain replaced data.
cassette = File.read!("#{@dummy_cassette_dir}/sensitive_data_in_request_header.json")
assert cassette =~ "\"X-My-Secret-Token\": \"***\""
refute cassette =~ "\"X-My-Secret-Token\": \"my-secret-token\""
ExVCR.Config.filter_request_headers(nil)
end
test "replace sensitive data in request options" do
ExVCR.Config.filter_request_options("basic_auth")
use_cassette "sensitive_data_in_request_options" do
body = HTTPoison.get!(@url, [], [hackney: [basic_auth: {"username", "password"}]]).body
assert body == "test_response"
end
# The recorded cassette should contain replaced data.
cassette = File.read!("#{@dummy_cassette_dir}/sensitive_data_in_request_options.json")
assert cassette =~ "\"basic_auth\": \"***\""
refute cassette =~ "\"basic_auth\": {\"username\", \"password\"}"
ExVCR.Config.filter_request_options(nil)
end
Allowed hosts
The :ignore_urls can be used to allow requests to be made to certain hosts.
setup do
ExVCR.Setting.set(:ignore_urls, [~/example.com/])
ExVCR.Setting.append(:ignore_urls, ~/anotherurl.com/)
end
test "an actual request is made to example.com" do
HTTPoison.get!("https://example.com/path?query=true")
HTTPoison.get!("https://anotherurl.com/path?query=true")
end
Ignoring query params in URL
If ExVCR.Config.filter_url_params(true) is specified, query params in URL
will be ignored when recording cassettes.
test "filter url param flag removes url params when recording cassettes" do
ExVCR.Config.filter_url_params(true)
use_cassette "example_ignore_url_params" do
assert HTTPoison.get!(
"http://localhost:34000/server?should_not_be_contained", []).body =~ ~r/test_response/
end
json = File.read!("#{__DIR__}/../#{@dummy_cassette_dir}/example_ignore_url_params.json")
refute String.contains?(json, "should_not_be_contained")
Removing headers from response
If ExVCR.Config.response_headers_blacklist(headers_blacklist) is specified,
the headers in the list will be removed from the response.
test "remove blacklisted headers" do
use_cassette "original_headers" do
assert Map.has_key?(HTTPoison.get!(@url, []).headers, "connection") == true
end
ExVCR.Config.response_headers_blacklist(["Connection"])
use_cassette "remove_blacklisted_headers" do
assert Map.has_key?(HTTPoison.get!(@url, []).headers, "connection") == false
end
ExVCR.Config.response_headers_blacklist([])
end
Matching Options
Matching against query params
By default, query params are not used for matching. In order to include query
params, specify match_requests_on: [:query] for use_cassette call.
test "matching query params with match_requests_on params" do
use_cassette "different_query_params", match_requests_on
