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.

SCAN and KEYS pattern matching

SCAN cursor [MATCH pattern] [COUNT count] and KEYS pattern filter keys using Redis glob patterns:

PatternMeaning
*any number of characters
?exactly one character
[aeiou]one character from the set
[^aeiou]one character not in the set
[a-z]character range inside a class
\?, \*, …match ?, * literally

Examples over TCP (RESP arrays):

# Keys whose name contains @ (typical JID-style lookups)
["SCAN", "0", "MATCH", "*@*"]
# Keys ending with a domain suffix
["SCAN", "0", "MATCH", "*@example.com", "COUNT", "100"]
# Same glob rules for KEYS
["KEYS", "session:*"]

FauxRedis applies MATCH filtering before COUNT limits the returned batch. For tests, the cursor is always returned as "0" with the full result set in one reply — enough for clients that loop until cursor zero, but not a faithful reproduction of Redis cursor-based iteration over large keyspaces.

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.