lostbean
ReqCassette - VCR-style testing for Req with async support
Hello Elixir community! ![]()
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:
- First request → Forwards to real server → Saves response to JSON →
Returns response - 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! ![]()
Resources
- Hex: hex.pm/packages/req_cassette
- Documentation: hexdocs.pm/req_cassette
- ReqLLM Integration Guide:
ReqLLM Integration - GitHub:
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
ReqCassette v0.2.0 Update 
Quick update on ReqCassette! v0.2.0 is now available with some nice
improvements:
What’s New:
with_cassette/3function API for cleaner test code
Human-readable cassette filenames (github_user.jsoninstead of
a1b2c3d4.json)
Multiple interactions per cassette file
Four recording modes: :replay,:record,:record_missing,:bypass
Sensitive data filtering (headers, regex-based redaction)
Configurable request matching
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:
- Hex: req_cassette | Hex
- Docs: ReqCassette v0.5.2 — Documentation
- Migration guide: https://hexdocs.pm/req_cassette/migration_v0.1_to_v0.2.html
Breaking changes from v0.1 - see migration guide.
lostbean
ReqCassette v0.3.0 – Safer Recording & Better Filtering
What’s New
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_missingwith:record
Better filtering
- New
filter_requestandfilter_responseoptions - Comprehensive filtering guide for protecting API keys
Quick Example
with_cassette "api_test",
[filter_request_headers: ["authorization"]],
fn plug ->
Req.get!("https://api.example.com", plug: plug)
end
mikehostetler
Nice work! Thanks for putting this together!!!







