Hex.pm Version waffle documentation

VCR-style HTTP recording and replay for Elixir's Req library. Record HTTP interactions once, replay them in tests forever—no external dependencies, fast tests, deterministic results.

Features

  • Zero app code changes - Works through Req.Test integration
  • Fast tests - Replay from cassettes, no network calls
  • Chronological ordering - Timestamp-based replay for concurrent requests
  • Four modes - Replay (default), Record new, Auto-record, Re-record all
  • Binary & streaming - Handles images, PDFs, SSE, chunked responses
  • Flexible organization - Named builders, custom paths, macro support
  • Test-friendly - Works with async tests and spawned processes

Quick Start

Installation

def deps do
  [
    {:req, "~> 0.5"},
    {:reqord, "~> 0.4.0"}
  ]
end

Basic Usage

defmodule MyApp.APITest do
  use Reqord.Case  # Instead of: use ExUnit.Case

  test "fetches user data" do
    {:ok, response} = Req.get("https://api.example.com/users/1")

    assert response.status == 200
    assert response.body["name"] == "John Doe"
  end
end

Record Cassettes

# Record on first run
REQORD=new_episodes mix test

# Subsequent runs replay from cassettes (no network calls)
mix test

That's it! Your tests now use recorded cassettes. 🎉

Recording Modes

Control how Reqord handles cassettes:

ModeEnvironment VariableBehavior
ReplayREQORD=none (default)Use cassettes, never hit network
Record newREQORD=new_episodesReplay existing, record new requests
StrictREQORD=onceReplay only, raise on missing cassettes
Re-recordREQORD=allAlways hit network, re-record everything

Per-Test Mode

@tag vcr_mode: :new_episodes
test "can record new requests" do
  # This test can record even if REQORD=none globally
end

Cassette Organization

Default: Module/Test Name

defmodule MyApp.UserAPITest do
  use Reqord.Case

  test "creates user" do
    # Cassette: test/support/cassettes/UserAPI/creates_user.jsonl
  end
end

Custom Name

@tag vcr: "my_custom_name"
test "example" do
  # Cassette: test/support/cassettes/my_custom_name.jsonl
end
# config/test.exs
config :reqord,
  cassette_path_builders: %{
    api: fn context -> "api/#{context.test}" end,
    llm: fn context ->
      provider = get_in(context, [:macro_context, :provider])
      "providers/#{provider}/#{context.test}"
    end
  }

# In tests
defmodule APITest do
  use Reqord.Case, cassette_path_builder: :api
end

defmodule LLMTest do
  use Reqord.Case, cassette_path_builder: :llm
end

Documentation

Guides

Common Tasks

Concurrent Requests

test "handles parallel requests" do
  task = Task.async(fn ->
    Req.get("https://api.example.com/data")
  end)

  Reqord.allow(MyApp.ReqStub, self(), task.pid)
  {:ok, response} = Task.await(task)
end

Custom Matchers

# Match on method, path, and body
@tag match_on: [:method, :path, :body]
test "strict matching" do
  Req.post(url, json: %{name: "Alice"})
end

Binary Data

Reqord automatically handles binary responses:

test "downloads image" do
  {:ok, resp} = Req.get("https://example.com/image.png")
  # Large binaries stored externally, replayed seamlessly
end

Streaming Responses

test "handles server-sent events" do
  {:ok, resp} = Req.get("https://api.example.com/stream")
  # Streaming responses captured and replayed
end

Configuration

# config/test.exs
config :reqord,
  default_mode: :none,
  cassette_dir: "test/support/cassettes",
  match_on: [:method, :uri]

See Advanced Configuration for all options.

How It Works

  1. First run: Reqord records HTTP requests/responses to cassette files (JSONL format)
  2. Subsequent runs: Requests are matched against cassettes and responses replayed
  3. Matching: By default, matches on HTTP method + URI (configurable)
  4. Ordering: Timestamp-based chronological replay handles concurrent requests

Request Matching

GET https://api.example.com/users?sort=name

Normalized: GET https://api.example.com/users?sort=name (params sorted)

Match cassette entry by: method + normalized URI + body hash

Replay recorded response

Cassette Format

Cassettes are stored as JSON Lines (.jsonl):

{"req":{"method":"GET","url":"..."},"resp":{"status":200,"body":"..."},"recorded_at":"2024-01-01T12:00:00.000000Z"}
{"req":{"method":"POST","url":"..."},"resp":{"status":201,"body":"..."},"recorded_at":"2024-01-01T12:00:01.123456Z"}

Comparison with ExVCR

FeatureReqordExVCR
Best forAPI clients built on ReqFull-fledged apps with various HTTP libraries
HTTP clientsReq onlyHTTPoison, HTTPotion, Hackney, and more
IntegrationReq.Test (no code changes)Wrap HTTP calls with use_cassette
Binary dataExternal storage for large filesInline Base64 encoding
StreamingFull SSE/chunked response supportStandard request/response pairs
Cassette writesAsync (non-blocking)Synchronous

Choose Reqord if: You're building an API client or library using Req and want zero application code changes.

Choose ExVCR if: You need to support multiple HTTP clients in a full application or use libraries other than Req.

Examples

Check out the examples/ directory for complete examples:

  • examples/macro_generated_tests.exs - Macro-generated test patterns
  • More examples in the documentation guides

License

MIT License - see LICENSE for details.

Credits

Inspired by ExVCR and Ruby's VCR.