lostbean

lostbean

ReqCassette - VCR-style testing for Req with async support

Hello Elixir community! :wave:

I’m excited to announce the first release of ReqCassette, a VCR-style record-and-replay library specifically designed for Req.

What is ReqCassette?

ReqCassette captures HTTP responses to JSON files (“cassettes”) and replays them in subsequent test runs, making your tests:

  • Faster - No network calls after initial recording
  • Deterministic - Same response every time
  • Offline-capable - Work without internet once cassettes are recorded
  • Cost-effective - Perfect for testing paid APIs (like LLM services!)

Why build this

ReqCassette leverages Req’s native testing infrastructure instead of global mocking, making it process-isolated and fully compatible with ExUnit’s async testing.

Quick Example

defmodule MyApp.APITest do
  use ExUnit.Case, async: true

  test "fetches user data" do
    # First run: records to cassette
    # Subsequent runs: instant replay!
    response = Req.get!(
      "https://api.example.com/users/1",
      plug: {ReqCassette.Plug, %{cassette_dir: "test/cassettes"}}
    )

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

Created with ReqLLM in mind

One of my favorite use cases is testing LLM applications with
ReqLLM:

{:ok, response} = ReqLLM.generate_text(
  "anthropic:claude-sonnet-4-20250514",
  "Explain recursion",
  max_tokens: 100,
  req_http_options: [
    plug: {ReqCassette.Plug, %{cassette_dir: "test/cassettes"}}
  ]
)

First call costs money and it’s slow, but every subsequent test is fast and
FREE - the response is replayed from the cassette! This makes testing with LLMs
much more practical.

How It Works

ReqCassette uses Req’s :plug option to intercept requests:

  1. First request → Forwards to real server → Saves response to JSON →
    Returns response
  2. Subsequent requests → Loads JSON cassette → Returns saved response

Cassettes are matched by HTTP method, path, query string, and request body,
creating a unique MD5 hash for the filename.

What’s Next?

Some ideas for future versions:

  • Additional recording modes (:once, :none, :all)
  • Custom cassette naming
  • Header-based matching
  • Cassette expiration/TTL
  • Request filtering/sanitization helpers

Feedback Welcome!

This is the first release, and I’d love to hear your thoughts:

  • Are there features you’d like to see?
  • Any issues or bugs to report?

Thanks for reading, and happy testing! :test_tube:

Resources


Note: Special thanks to the Req maintainers for building such a fantastic HTTP
client with great testing support, and to the ReqLLM maintainers for creating a
promising and foundational library that makes Elixir shine in the new LLM
space!

Most Liked

lostbean

lostbean

ReqCassette v0.2.0 Update :clapper_board:

Quick update on ReqCassette! v0.2.0 is now available with some nice
improvements:

What’s New:

  • :sparkles: with_cassette/3 function API for cleaner test code
  • :memo: Human-readable cassette filenames (github_user.json instead of
    a1b2c3d4.json)
  • :package: Multiple interactions per cassette file
  • :level_slider: Four recording modes: :replay, :record, :record_missing, :bypass
  • :locked: Sensitive data filtering (headers, regex-based redaction)
  • :bullseye: Configurable request matching
  • :artist_palette: Pretty-printed JSON with native JSON objects (40% smaller)

Example:

import ReqCassette

test "API integration" do
  with_cassette "github_user", [mode: :replay], fn plug ->
    response = Req.get!("https://api.github.com/users/wojtekmach", plug: plug)
    assert response.status == 200
  end
end

First run records, subsequent runs replay instantly (no network, async-safe).

Links:

:warning: Breaking changes from v0.1 - see migration guide.

lostbean

lostbean

ReqCassette v0.3.0 – Safer Recording & Better Filtering

What’s New

:shield: Safer :record mode (breaking change)

  • Fixed dangerous behavior where multi-request tests would lose interactions
  • Simplified API: 4 recording modes → 3
  • Migration: just replace :record_missing with :record

:locked: Better filtering

Quick Example

with_cassette "api_test",
  [filter_request_headers: ["authorization"]],
  fn plug ->
    Req.get!("https://api.example.com", plug: plug)
  end
mikehostetler

mikehostetler

Nice work! Thanks for putting this together!!!

Where Next?

Popular in Announcing Top

sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
Flo0807
Hello everyone! I am excited to share our heart project Backpex with you. After building several Phoenix applications, we realized that...
New
BartOtten
Phoenix Live Favicon Favicon manipulation for Phoenix Live A lib enabling dynamic favicons in Phoenix Live View applications. To sho...
New
pcharbon
This is a new extension for the Ash framework that lets you use the Commanded library in a more declarative manner and removes most of th...
New
mikehostetler
Hey everyone! I’m excited to share ReqLLM - a new approach to LLM interactions in Elixir that I’ve been working on. After building agent...
New
bradley
I’ve been working with Claude Code extensively and absolutely love it. However, I’ve come across the challenge of managing configuration ...
New
garrison
Hobbes is a scalable, fault-tolerant transactional record store written in Elixir. Hobbes is designed to be: Scalable - Hobbes can sha...
New
lostbean
Hello Elixir community! :wave: I’m excited to announce the first release of ReqCassette, a VCR-style record-and-replay library specifica...
New
leandrocp
MDEx is a fast and extensible Markdown parser and formatter. Fast Leverage Rust to parse, manipulate and render documents using: comra...
New
sodapopcan
I’ve heard that if you’re not embarrassed by v1 of your product Vim plugin then you’ve released too late. I’ve been sitting on this the ...
New

Other popular topics Top

JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

We're in Beta

About us Mission Statement