Reqord.Case (reqord v0.4.0)

View Source

ExUnit case template for using Reqord in tests.

This module provides automatic cassette management and Req.Test integration for your tests.

Usage

defmodule MyAppTest do
  use Reqord.Case

  test "makes API call" do
    # Requests will automatically use cassettes
    {:ok, resp} = Req.get("https://api.example.com/data")
    assert resp.status == 200
  end

  @tag vcr: "custom/cassette/name"
  test "with custom cassette name" do
    # Will use custom cassette name instead of auto-generated
  end

  @tag req_stub_name: MyApp.CustomStub
  test "with custom stub name" do
    # Will use custom Req.Test stub name
  end
end

Configuration

Set the VCR record mode via the REQORD environment variable or application config.

Environment Variable

  • REQORD=once - Strict replay, raise on new requests
  • REQORD=new_episodes - Replay existing, record new requests
  • REQORD=all - Always hit live network and re-record
  • REQORD=none - Never record, never hit network (default)

Application Config

You can also configure the default mode in your config files:

config :reqord, default_mode: :none

Per-Test Mode

Override mode for specific tests using tags:

@tag vcr_mode: :new_episodes
test "allows new recordings" do
  # This test will record new requests
end

Per-Test Matchers

Override matchers for specific tests:

@tag match_on: [:method, :path, :body]
test "matches on method, path, and body" do
  # This test uses custom matchers
end

Cassette Naming

Reqord supports multiple ways to organize your cassettes, with the following priority:

1. Explicit Path (:vcr_path tag)

Use the :vcr_path tag to explicitly set the cassette path:

@tag vcr_path: "providers/google/gemini-2.0-flash/basic_chat"
test "basic chat" do
  # Uses "providers/google/gemini-2.0-flash/basic_chat.jsonl"
end

Define reusable builders in config and reference them by name:

# config/test.exs
config :reqord,
  cassette_path_builders: %{
    llm_provider: fn context ->
      provider = get_in(context, [:macro_context, :provider]) || "default"
      model = get_in(context, [:macro_context, :model]) || "default"
      "providers/#{provider}/#{model}/#{context.test}"
    end,
    api: fn context -> "api/#{context.test}" end
  }

Then use them in test modules:

defmodule MyApp.LLMTest do
  use Reqord.Case, cassette_path_builder: :llm_provider
  # Cassettes: providers/google/gemini-flash/test_name.jsonl
end

defmodule MyApp.APITest do
  use Reqord.Case, cassette_path_builder: :api
  # Cassettes: api/test_name.jsonl
end

defmodule MyApp.UtilsTest do
  use Reqord.Case
  # Cassettes: Utils/test_name.jsonl (default)
end

3. Global Path Builder

Configure a single builder for all tests:

config :reqord,
  cassette_path_builder: fn context ->
    provider = context[:provider] || "default"
    "#{provider}/#{context.test}"
  end

4. Simple Name Override (:vcr tag)

Override with a simple name using the :vcr tag:

@tag vcr: "my_custom_cassette"
test "example" do
  # Uses "my_custom_cassette.jsonl"
end

5. Default Behavior

By default, cassettes are named after the test module and test name: "ModuleName/test_name.jsonl"

Macro-Generated Tests

For tests generated by macros that need to access compile-time variables for cassette naming, use set_cassette_context/1 with named builders:

# config/test.exs
config :reqord,
  cassette_path_builders: %{
    llm_provider: fn context ->
      provider = get_in(context, [:macro_context, :provider]) || "default"
      model = get_in(context, [:macro_context, :model]) || "default"
      "providers/#{provider}/#{model}/#{context.test}"
    end
  }

defmodule MyLLMTest do
  use Reqord.Case, cassette_path_builder: :llm_provider

  for {provider, models} <- [{"google", ["gemini-flash"]}, {"openai", ["gpt-4"]}] do
    @provider provider
    for model <- models do
      @model model

      describe "#{provider}:#{model}" do
        setup do
          # Provide macro context for cassette naming
          Reqord.Case.set_cassette_context(%{
            provider: @provider,
            model: @model
          })
          :ok
        end

        test "generates text" do
          # Each provider/model gets its own cassette:
          # providers/google/gemini-flash/test_generates_text.jsonl
          # providers/openai/gpt-4/test_generates_text.jsonl
        end
      end
    end
  end
end

See MACRO_SUPPORT.md for detailed examples.

Spawned Processes

If your test spawns processes that make HTTP requests, you need to allow them:

test "with spawned process" do
  task = Task.async(fn ->
    Req.get("https://api.example.com/data")
  end)

  Reqord.allow(MyApp.ReqStub, self(), task.pid)
  Task.await(task)
end

Summary

Functions

Sets cassette context for the current test process.

Functions

set_cassette_context(context)

@spec set_cassette_context(map()) :: :ok

Sets cassette context for the current test process.

This is useful for macro-generated tests where compile-time variables (like module attributes) need to be included in cassette naming.

The context map will be merged with test tags and made available to the :cassette_path_builder function.

Examples

# In a macro-generated test
for model <- ["gpt-4", "gemini-flash"] do
  @model model

  describe "#{model}" do
    setup do
      Reqord.Case.set_cassette_context(%{model: @model})
      :ok
    end

    test "example" do
      # Cassette naming can now access the model
    end
  end
end

Usage with cassette_path_builder

# config/test.exs
config :reqord,
  cassette_path_builder: fn context ->
    # context.macro_context contains the data from set_cassette_context
    model = context.macro_context[:model] || "default"
    "#{model}/#{context.test}"
  end