Passby

Hex PackageHex DocsLicenseCI

A 100% Elixir, 0-dependency mock HTTP server designed for testing HTTP clients and integrations.

Passby is a lightweight, drop-in replacement for Bypass. It provides the exact same API and semantics without dragging in plug, plug_cowboy, cowboy, cowlib, ranch, or cowboy_telemetry.


Features


Installation

Add passby to your mix.exs dependencies for the test environment:

def deps do
[
{:passby, "~> 0.1.0", only: :test}
]
end

Quick Start

defmodule MyClientTest do
use ExUnit.Case, async: true
setup do
bypass = Passby.open()
{:ok, bypass: bypass}
end
test "fetches user profile successfully", %{bypass: bypass} do
Passby.expect_once(bypass, "GET", "/api/users/42", fn conn ->
conn
|> Passby.put_resp_header("content-type", "application/json")
|> Passby.resp(200, ~s({"id": 42, "name": "Alice"}))
end)
url = "http://127.0.0.1:#{bypass.port}/api/users/42"
assert {:ok, %{"name" => "Alice"}} = MyClient.get_user(url)
end
end

Migrating from Bypass

Migrating from Bypass to Passby requires zero changes to test logic:

  1. Replace {:bypass, ...} with {:passby, "~> 0.1.0", only: :test} in mix.exs.
  2. Replace Bypass. calls with Passby.:
# Before (Bypass)
setup do
bypass = Bypass.open()
{:ok, bypass: bypass}
end
# After (Passby)
setup do
bypass = Passby.open()
{:ok, bypass: bypass}
end

Handlers receive a %Passby.Conn{} struct which works seamlessly with either Passby.Conn / Passby functions or Plug.Conn if you have Plug in your project:

Passby.expect(bypass, "POST", "/messages", fn conn ->
# Using Passby helpers:
conn
|> Passby.put_resp_header("content-type", "application/json")
|> Passby.resp(201, ~s({"status": "created"}))
# Or using Plug.Conn if available in your project:
# Plug.Conn.resp(conn, 201, ~s({"status": "created"}))
end)

Usage Patterns

1. Specific Request Expectations (expect/4 and expect_once/4)

# Matches only GET requests to /health
Passby.expect(bypass, "GET", "/health", fn conn ->
Passby.resp(conn, 200, "OK")
end)
# Consumed after the first request
Passby.expect_once(bypass, "POST", "/checkout", fn conn ->
assert conn.req_body =~ "item_123"
Passby.resp(conn, 200, ~s({"order_id": 999}))
end)

2. General Fallback Stubs (stub/4)

Passby.stub(bypass, "GET", "/config", fn conn ->
Passby.resp(conn, 200, ~s({"env": "test"}))
end)

3. Simulating Outages and Downtime (down/1 and up/1)

test "handles server outages gracefully", %{bypass: bypass} do
Passby.down(bypass)
url = "http://127.0.0.1:#{bypass.port}/api"
assert {:error, :econnrefused} = MyClient.get(url)
Passby.up(bypass)
Passby.expect(bypass, "GET", "/api", fn conn ->
Passby.resp(conn, 200, "recovered")
end)
assert {:ok, "recovered"} = MyClient.get(url)
end

Quality & Compliance

Passby is fully tested, typed, and documented:

To run the complete check suite locally:

mix check

License

MIT License. Copyright (c) 2026 Altenwald Solutions, S.L.