FauxRedis

CI

A controllable, Redis-compatible dummy server implemented in Elixir/OTP, designed specifically for integration testing.

FauxRedis speaks the Redis RESP protocol over TCP, but exposes a rich programmatic API for stubbing, expectations and fault injection – allowing you to simulate everything from normal key-value operations to timeouts, disconnects and protocol errors.

This project is intentionally not a production Redis replacement. It is a test tool with predictable behaviour and powerful mocking.

Name

FauxRedis – a short, memorable name for a fake Redis server aimed at integration tests. Suggested GitHub repo: faux_redis (slug: faux-redis).

Features

Installation

Add to your mix.exs dependencies.

def deps do
  [
    {:faux_redis, "~> 1.0", only: :test}
  ]
end

Then fetch dependencies:

mix deps.get

Basic usage

You usually run FauxRedis as part of your test supervision tree or via the provided ExUnit case template.

Starting manually inside a test

Main options for start_link/1: :port (default 0 = random), :mode (:mock_first, :stateful_only, :mock_only), :name (registered name), :ip (bind address, default {127,0,0,1}), :require_auth?, :password.

test "simple PING/ECHO with random port" do
  {:ok, server} = FauxRedis.start_link(port: 0, mode: :mock_first)

  port = FauxRedis.port(server)

  {:ok, socket} =
    :gen_tcp.connect('localhost', port, [:binary, packet: :raw, active: false])

  payload =
    [
      ["PING"],
      ["ECHO", "hi"]
    ]
    |> Enum.map(&FauxRedis.RESP.encode/1)
    |> IO.iodata_to_binary()

  :ok = :gen_tcp.send(socket, payload)
  {:ok, data} = :gen_tcp.recv(socket, 0, 1_000)

  assert {:ok, "PONG"} = FauxRedis.RESP.decode_exactly(data |> String.split_at(8) |> elem(0))

  :gen_tcp.close(socket)
  FauxRedis.stop(server)
end

Using the ExUnit helper

The library ships with FauxRedis.Case, which starts an isolated server per test and injects redis_server and redis_port into the test context. It also imports convenience stub/3 and expect/3 that take the context map as the first argument (they delegate to FauxRedis.stub/3 / FauxRedis.expect/3).

defmodule MyApp.RedisIntegrationTest do
  use FauxRedis.Case, async: true

  test "basic interaction", %{redis_server: server, redis_port: port} do
    {:ok, _rule} = FauxRedis.stub(server, :get, "value")

    {:ok, socket} =
      :gen_tcp.connect('localhost', port, [:binary, packet: :raw, active: false])

    payload =
      ["GET", "foo"]
      |> FauxRedis.RESP.encode()
      |> IO.iodata_to_binary()

    :ok = :gen_tcp.send(socket, payload)
    {:ok, data} = :gen_tcp.recv(socket, 0, 1_000)
    assert {:ok, "value"} = FauxRedis.RESP.decode_exactly(data)

    :gen_tcp.close(socket)
  end

  test "using context helpers", %{redis_port: port} = ctx do
    {:ok, _rule} = stub(ctx, :ping, "PONG")
    # ...
  end
end

Mocking API

The public API lives in the FauxRedis module:

Low-level RESP helpers: FauxRedis.RESP (encode/1, decode/1, decode_exactly/1, …) for building or parsing wire payloads in tests.

Stubbing a single command

{:ok, server} = FauxRedis.start_link(port: 0)

# Always respond to GET foo with "bar"
{:ok, _rule} =
  FauxRedis.stub(server, {:command, :get, ["foo"]}, "bar")

Sequential responses

# First GET foo -> "one"
# Second GET foo -> "two"
# Third and subsequent GET foo -> nil
{:ok, _rule} =
  FauxRedis.stub(server, :get, ["one", "two", nil])

Fault injection

# PING:
#   1st call -> reply "SLOW" after 500ms
#   2nd call -> no reply (:timeout or :no_reply)
#   3rd call -> close connection
{:ok, _rule} =
  FauxRedis.stub(server, :ping, [
    {:delay, 500, "SLOW"},
    :timeout,
    :close
  ])

Matching by regex, arguments and connection id

# Match any GET whose key starts with "session:"
{:ok, _rule} =
  FauxRedis.stub(server, {:command, :get, [~r/^session:/]}, fn cmd ->
    {:error, "ERR sessions disabled in tests"}
  end)

You can also restrict rules to specific connections via :conn_id or :conn_ids options when calling stub/2/expect/3 (see moduledoc of FauxRedis.MockRule for details).

Inspecting call history

calls = FauxRedis.calls(server)

assert [
         %{
           name: "GET",
           args: ["foo"],
           conn_id: _,
           db: 0,
           timestamp: _,
           rule_id: _,
           action: _
         }
       ] = calls

Resetting between tests

FauxRedis.reset!(server)

This clears:

Using with ejabberd / XMPP

FauxRedis is designed to play nicely with typical Redis clients used by ejabberd and other XMPP systems:

Because the server is a normal OTP process, you can run multiple instances in a single test suite to simulate multiple Redis backends if necessary.

Architectural overview

Limitations vs real Redis

FauxRedis is not a full Redis implementation. Notable limitations:

If you need more real-world behaviour, consider using a real Redis instance for system tests and FauxRedis for fine-grained, deterministic integration tests and fault injection.

Running locally

After cloning the repository:

mix deps.get
mix test

The codebase is compatible with OTP 24 and newer; the CI matrix runs tests on OTP 24, 25 and 26 to keep this guarantee.

License

This project is licensed under the MIT License – see the LICENSE file.