FauxMQ

CI

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

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):

def deps do
  [
    {:faux_mq, "~> 1.0"}
  ]
end

Then fetch dependencies:

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}).
:portNot 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

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:

{: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):

FauxMQ.stub(server, %{method_name: :basic_publish}, :close_connection)

Sequential responses

You can compose actions into sequences and delays:

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:

Fault injection

Simulate an authentication failure during handshake:

FauxMQ.stub(
  server,
  %{method_name: :connection_start_ok},
  :protocol_error
)

Simulate a slow broker that never responds:

FauxMQ.stub(
  server,
  %{method_name: :basic_publish},
  :no_reply
)

Simulate random connection drops:

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):

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:

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:

{: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

Example integration test module

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):

config :faux_mq, debug: true

At runtime (e.g. in test_helper.exs or a single test):

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):

# 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

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:

Limitations

License

MIT. See LICENSE file for details.