# FauxMQ
[](https://github.com/aszymanskiit/faux-mq/actions/workflows/ci.yml)
FauxMQ is a **dummy AMQP 0-9-1 broker** implemented in Elixir/OTP.
It behaves like a real TCP AMQP endpoint from the client's perspective, while
giving tests **full control over the broker's behaviour**: handshakes, channels,
queues, exchanges, publishes, consumes, faults, timeouts and protocol-level
edge cases.
It is designed primarily for **integration testing ejabberd/XMPP** and other
systems that speak AMQP 0-9-1.
## Features
- **Real TCP AMQP 0-9-1 server** (listener, framing, handshake, channels)
- **Controllable mock/stub API** (`stub/3`, `expect/4`) for per-method behaviour
- **Call history** (`calls/1`) for assertions
- **Rules override defaults**: if a stub/expect matches an incoming method, that action runs; otherwise the built-in handler answers (handshake, channel lifecycle, in-memory queues/bindings, `basic.publish` / `basic.get` / `basic.consume` flow)
- **Per-server isolation**: each `FauxMQ.start_link/1` has its own listener, mock process, and broker state
- Intended as a **`mix` dependency** in test (or dev) environments
> Note: FauxMQ is **not a production broker**. It aims at protocol fidelity and
> excellent test ergonomics, not performance or completeness of broker semantics.
## Installation
Add FauxMQ to your `mix.exs` (requires Elixir ~> 1.13):
```elixir
def deps do
[
{:faux_mq, "~> 1.0"}
]
end
```
Then fetch dependencies:
```bash
mix deps.get
```
The `:faux_mq` application starts a small supervision tree (including a `Registry` for internal use). **No TCP broker is listening until** you call `FauxMQ.start_link/1` (or add `FauxMQ.child_spec/1` to your own supervisor).
## Starting the server (`start_link/1`)
Relevant options:
| Option | Meaning |
|--------|--------|
| `:host` | Bind address as an IP tuple (default: `config :faux_mq, :default_host`, usually `{127, 0, 0, 1}`). |
| `:port` | **Not** the usual “`0` = OS ephemeral” shortcut by itself: `0` means “use `config :faux_mq, :default_port`”. The default config sets `default_port: 0`, which makes the OS choose a free port. If you set `default_port` to e.g. `5672`, then passing `port: 0` resolves to `5672`. Any other positive integer binds that port; if the port is already in use (`:eaddrinuse`), the server **falls back** to an ephemeral port. |
You can still use `FauxMQ.port/1` / `FauxMQ.endpoint/1` after start to read the actual port.
## Basic usage in ExUnit
```elixir
defmodule MyApp.AMQPTest do
use ExUnit.Case, async: false
test "uses FauxMQ as AMQP broker" do
{:ok, server} = FauxMQ.start_link(port: 0)
endpoint = FauxMQ.endpoint(server)
# inject endpoint.host/endpoint.port into your system under test here
# exercise your code ...
calls = FauxMQ.calls(server)
assert Enum.any?(calls, fn %{context: ctx} ->
ctx.method_name == :basic_publish
end)
:ok = FauxMQ.stop(server)
end
end
```
## Stubbing a single AMQP method
Example: when a client uses `basic.publish`, close the connection:
```elixir
{:ok, server} = FauxMQ.start_link(port: 0)
FauxMQ.stub(server, %{class_id: 60, method_id: 40}, :close_connection)
```
You can also match on `:method_name`, and optionally narrow by `:connection_id`, `:channel_id`, or a custom `{:predicate, fn ctx -> ... end}` (see types in `FauxMQ.Types`):
```elixir
FauxMQ.stub(server, %{method_name: :basic_publish}, :close_connection)
```
## Sequential responses
You can compose actions into sequences and delays:
```elixir
FauxMQ.expect(
server,
%{method_name: :basic_publish},
2,
{:sequence,
[
{:delay, 200, :no_reply},
{:reply,
{:frames,
[
FauxMQ.Protocol.build_connection_close(500, "first failure", 60, 40)
]}}
]}
)
```
This example:
- applies to the first two `basic.publish` calls
- waits 200ms
- sends a mocked `connection.close` error
## Fault injection
Simulate an authentication failure during handshake:
```elixir
FauxMQ.stub(
server,
%{method_name: :connection_start_ok},
:protocol_error
)
```
Simulate a slow broker that never responds:
```elixir
FauxMQ.stub(
server,
%{method_name: :basic_publish},
:no_reply
)
```
Simulate random connection drops:
```elixir
FauxMQ.stub(
server,
%{method_name: :basic_publish},
:close_connection
)
```
## Publish / consume scenario
FauxMQ can push deliveries to clients, e.g. for `basic.consume` tests (the map keys match `FauxMQ.Types.push_delivery_spec/0`):
```elixir
delivery = %{
channel_id: 1,
consumer_tag: "ctag-1",
exchange: "amq.direct",
routing_key: "queue",
payload: "hello",
delivery_tag: 1,
redelivered: false
}
FauxMQ.push_delivery(server, delivery)
```
The map may also include optional `header_payload` for content header bytes (see `push_delivery_spec` in `FauxMQ.Types`).
From the client's perspective this is indistinguishable from a normal broker
delivering a message after `basic.consume`.
## Pushing arbitrary server frames
For lower-level tests you can inject a raw frame (method/header/body/heartbeat) with `FauxMQ.push_frame/2`:
```elixir
FauxMQ.push_frame(server, %{
type: :method,
channel: 1,
payload: <<...>>
})
```
See `push_frame_spec` / `frame_spec` in `FauxMQ.Types`.
## Using FauxMQ as ejabberd AMQP endpoint
In your integration test:
```elixir
{:ok, faux} = FauxMQ.start_link(port: 0)
endpoint = FauxMQ.endpoint(faux)
ejabberd_config =
base_config()
|> put_in([:amqp, :host], :inet.ntoa(endpoint.host) |> to_string())
|> put_in([:amqp, :port], endpoint.port)
# start ejabberd using ejabberd_config
```
You can now run ejabberd's AMQP-based code against FauxMQ. Use `FauxMQ.stub/3`
and `FauxMQ.expect/4` to script failures, reconnections, nack/unroutable
behaviour, etc.
### Resetting state between tests
- **`FauxMQ.reset!/1`** — Clears **stub/expect rules and call history** on that server's mock process only. In-memory queues, bindings, and consumers on the broker side **stay**.
- **`FauxMQ.reset_test_broker_state/1`** — Clears mocks/history **and** in-memory queues, bindings, and consumers (TCP connections are not force-closed). Prefer this when published messages or queue state must not leak across examples in a long `mix test` run.
## Example integration test module
```elixir
defmodule MyApp.EjabberdIntegrationTest do
use ExUnit.Case, async: false
test "ejabberd publishes presence events over AMQP" do
{:ok, faux} = FauxMQ.start_link(port: 0)
endpoint = FauxMQ.endpoint(faux)
# configure and start ejabberd using endpoint.host/endpoint.port
# simulate XMPP activity that should trigger AMQP publish
calls = FauxMQ.calls(faux)
assert Enum.any?(calls, fn %{context: ctx} ->
ctx.method_name == :basic_publish and ctx.channel_id == 1
end)
:ok = FauxMQ.stop(faux)
end
end
```
## Debug logging
FauxMQ can emit verbose protocol and connection logs (handshake, frames, accept, lifecycle). They are **off by default**. To turn them on, set the `:debug` config to `true`.
**In config (e.g. `config/test.exs` or `config/dev.exs`):**
```elixir
config :faux_mq, debug: true
```
**At runtime (e.g. in `test_helper.exs` or a single test):**
```elixir
Application.put_env(:faux_mq, :debug, true)
```
When `debug` is `true`, logs use standard Elixir `Logger` at levels `:info`, `:debug`, `:warning`, `:error` (e.g. `[FauxMQ.Connection]`, `[FauxMQ.Server]`, `[FauxMQ.Protocol]`). When `false` (default), none of these internal logs are printed.
Other application env keys used by the library include `:default_host`, `:default_port`, and `:heartbeat_interval` (see `config/config.exs` in this repo).
**Example (tests with debug on only for one test file):**
```elixir
# test/my_amqp_test.exs
defmodule MyApp.MyAmqpTest do
use ExUnit.Case, async: false
setup do
Application.put_env(:faux_mq, :debug, true)
on_exit(fn -> Application.put_env(:faux_mq, :debug, false) end)
:ok
end
test "something with AMQP" do
# ... FauxMQ logs will appear
end
end
```
## Running the project
```bash
mix deps.get
mix test
```
This repository does **not** include a `Dockerfile`; use a local Elixir/OTP install or any image that matches `mix.exs` (`elixir: "~> 1.13"`).
## CI
GitHub Actions workflows include:
- **CI** (`.github/workflows/ci.yml`): `mix format --check-formatted`, `mix credo --strict`, `mix test` on several OTP versions.
- **Dialyzer** (`.github/workflows/dialyzer.yml`): `mix dialyzer` (separate job with PLT caching).
## Limitations
- **Framing and parsing** follow AMQP 0-9-1; supported **methods and broker semantics** are those needed for integration-style testing (handshake, channel lifecycle, routing to in-memory queues, consumers, etc.), not full RabbitMQ parity.
- Routing is **simplified** (e.g. bindings and default-exchange behaviour are implemented in a minimal way compared to a real broker).
- Many methods can still be **intercepted** via `stub/3` and `expect/4` for fault injection or custom replies.
- The focus is on **protocol-level testing ergonomics** (mocks, history, pushed deliveries/frames), not throughput or production-grade queue semantics.
## License
MIT. See `LICENSE` file for details.