lostbean
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:
https://github.com/lostbean/req_cassette
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!
Trending in Announcing
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #blog-post
- #elixirconf-us
- #elixir-ls
- #ai
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
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 codegithub_user.jsoninstead ofa1b2c3d4.json):replay,:record,:record_missing,:bypassExample:
First run records, subsequent runs replay instantly (no network, async-safe).
Links:
mikehostetler
Nice work! Thanks for putting this together!!!
lostbean
ReqCassette v0.3.0 – Safer Recording & Better Filtering
What’s New
:recordmode (breaking change):record_missingwith:recordfilter_requestandfilter_responseoptionsQuick Example
tmbb
I am using ReqCassette to test a wrapper I’ve written for the Google Gemini API.I am not very happy with the wrappers I’ve found, so I’ve written my own - actually most of the code and the module docs are automatically generated from the JSON API spec.
One thing I want to be able to test is polling the same URL with the same query params to see if the batch has completed. This is clearly a stateful interaction. Even though this is a bit of a dumb thing to test, it’s actually really useful to be able to replay this interaction so that I can test/prototype new ways of displaying progress (progress bar, spinner, whatever).
Apparently, ReqCassette will not store the sequence of requests, but instead will save the request/response pair once, and will continue to return that result forever (I got this idea from inspecting the cassettes, not from inspecting the code).
Is this interpretation right? Is there any way of using ReqCassette for propertly stateful interactions?
lostbean
ReqCassette v0.4.0 - Templating & Better Debugging
Quick update on ReqCassette since 0.3.1
What’s New
Templating - One cassette can now handle multiple requests with different dynamic values. Great for APIs that return unique IDs, timestamps, or when testing with varying inputs.
LLM Presets - Built-in patterns for Anthropic and OpenAI APIs. No more cassette mismatches from
msg_*orchatcmpl-*IDs:Mismatch Diagnostics - When replay fails, you now get detailed info showing exactly which fields didn’t match:
Debug Tools - New
mix req_cassette.inspecttask to inspect cassette contents, anddebug: trueoption for template troubleshooting.Links
lostbean
Thanks for giving it a try @tmbb!
I have been able to test a multi-turn conversation with LLM APIs, where there is a chain of messages one after another that runs, and the cassette can store the sequence.
It’s not completely clear to me about what you mean by stateful interaction, but maybe the feature that I just released about templates could be useful here. Where we can parametrize the recorded cassettes, so we can pattern match against that and update the response.
Again, the use cases I have been using are related to LLMs. But in my case, I have some information on the input request that changes over time, but that’s predictable, so I can create a template and have it respond accordingly.
Let me know if that goes in the direction that helps you in your use case. I am definitely open to exploring ways to make the library more powerful.
tmbb
In pseudocode, I am doing the following:
When I call this with a cassette (I have my custom logic to get the plug into the request from the Process dictionary, assume that part works), it loops forever because apparently it always sees the same response in which the batch is not done.
lostbean
ReqCassette v0.5.0 - Sequential Matching for Stateful API Testing
What’s New in v0.5.0
Sequential Matching
By default, ReqCassette uses first-match: the same request always returns the
same response. This works for most tests, but not for polling scenarios
where identical requests should return different responses over time.
Now you can enable sequential matching:
Requests match interactions in order (request 1 → interaction 0, request 2 →
interaction 1, etc.).
Cross-Process Support
If you’re making HTTP requests from spawned processes (
Task.async,GenServer, etc.) with sequential matching, use a shared session:Templates Auto-Enable Sequential
If you’re using templating (
template: [...]), sequential matching is nowautomatically enabled - no need to add
sequential: true.Backward Compatibility
Default behavior is unchanged. Existing tests work without modification.
Links
Feedback welcome!
lostbean
Ah! I got it and I just released
0.5.0adds exactly what I think you need - sequential matching.Enable it with
sequential: true:When recording, each request hits the real API and stores a new interaction.
When replaying, requests match interactions in order (request 1 →
interaction 0, request 2 → interaction 1, etc.) instead of first-match.
Your cassette will look something like:
On replay, each poll gets the next response in sequence until it sees
done: true.One caveat: If your polling code runs in spawned processes (Task.async,
GenServer), you’ll need a shared session:
But if everything runs in a single process (which sounds like your case), just
sequential: trueshould work.Let know how it goes!
sergio
Thank you this is exactly what I was looking for and it works really well!