Skip to content

Latest commit

 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Passby

Hex Package Hex Docs License: MIT CI Paypal: Donation Patreon: Donation

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

  • 0 Runtime Dependencies: Built entirely on standard Erlang/OTP (:gen_tcp, :inet, :erlang.decode_packet) and standard Elixir.
  • Drop-in Bypass API: Identical function signatures (open/1, expect/2,4, expect_once/2,4, stub/4, pass/1, down/1, up/1).
  • Plug.Conn Compatibility: Passby.Conn implements the same fields and helper functions (resp/3, send_resp/1, get_req_header/2, put_resp_header/3, fetch_query_params/1).
  • Params Parsing: conn.query_params and conn.params are decoded exactly like Bypass/Plug do, including bracket notation (filter[name]=Manuel becomes %{"filter" => %{"name" => "Manuel"}}) and lists (tags[]=a&tags[]=b).
  • Route Patterns: paths accept :param segments (/users/:id); captured values land in conn.path_params and are merged into conn.params, just like Bypass.
  • Concurrent & Isolated: Each test can spin up its own instance on an ephemeral dynamic port.
  • Outage Simulation: Easily simulate network disconnects and connection-refused errors with Passby.down/1 and Passby.up/1.

Installation

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

def deps do
  [
    {:passby, "~> 0.2.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)

    assert {:ok, %{"name" => "Alice"}} = MyClient.get_user("#{bypass.url}/api/users/42")
  end
end

Migrating from Bypass

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

  1. Replace {:bypass, ...} with {:passby, "~> 0.2.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 with query_params and params already populated (bracket notation and lists decoded just like Bypass). Calling Passby.Conn.fetch_query_params/1 again is a safe no-op, kept for Plug.Conn parity.

The struct 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)

Compatibility scope

test/bypass_compat_test.exs ports every scenario from the Bypass test suite. The following API is supported with identical semantics:

open/1, expect/2, expect/4, expect_once/2, expect_once/4, stub/4, pass/1, down/1, up/1, :param route patterns, conn.params / conn.query_params / conn.path_params / conn.port, and route redefinition (last definition wins).

The following Bypass behaviour is not yet implemented in Passby (test/bypass_compat_test.exs covers each of these, asserting the current behaviour):

Bypass Passby (current)
Automatic verification on test exit (expect raises if never called, expect_once raises if called twice) No automatic verification; an unmet expectation is ignored and an unexpected request returns 500
verify_expectations!/1 and the :test_framework / ESpec integration Not implemented
expect/3 and expect/5 with an exact expected request count Not implemented (expect is "one or more", expect_once is "at most once")
pass/1 marks the in-flight request as arrived pass/1 clears every expectation and stub
down/1 blocks until in-flight handlers finish down/1 closes the listening socket immediately

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)

# Route patterns: `:param` segments are captured into conn.path_params / conn.params
Passby.expect(bypass, "GET", "/users/:id", fn conn ->
  assert conn.path_params == %{"id" => "42"}
  Passby.resp(conn, 200, ~s({"id": #{conn.params["id"]}}))
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 = Passby.url(bypass, "/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:

  • 100% Doctor documentation & spec coverage
  • Zero Dialyzer warnings
  • Strict Credo style checks
  • 90% test coverage

To run the complete check suite locally:

mix check

License

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

About

A 100% Elixir, 0-dependency HTTP mock server for testing HTTP clients. Drop-in replacement for Bypass.

Resources

Stars

12 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages