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

Schultzer
Hey there, I wrote this low-level library recently, its goal is simply to lower the barrier between Elixir and SQL, and it does that by p...
New
moxley
An Elixir implementation of the popular PostCSS library, providing CSS parsing, AST manipulation, and stringification capabilities. Feat...
New
isaias-dias-machado
IEx’s h macro is great but it lacks a pager, so I built a small tool that caches documentation and lets you fuzzy search through it in yo...
New
mikehostetler
I’m falling in love with the Req Plugin pattern. It has limits, but when it works - it’s good. I ported over my Tesla based Fly Machines...
New
kasvith
Hello Everyone, I was working with some HTML-to-Markdown libraries and ran into a few issues when converting a complex markup file to Ma...
New
fteschke
Announcing: playwright_ex A simple Elixir client for the Playwright node.js server. Automate browsers like Chromium, Firefox, Safari an...
New
Antrater
Hi there! At Moon Design System, we have been working hard for the past six months on the next generation of our LiveView component libra...
New
leandrocp
MDEx is a fast and extensible Markdown parser and formatter. Fast Leverage Rust to parse, manipulate and render documents using: comra...
New
mattmower
I’m pleased to announce that we’ve released DemoGen v0.1.7, a library for creating repeatable demo scenarios in your Ecto-based SaaS appl...
New
germsvel
PhoenixTest provides a unified way of writing feature tests – regardless of whether you’re testing LiveView pages or static pages. It al...
New

Other popular topics Top

vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
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
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
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
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New

We're in Beta

About us Mission Statement