FauxRedis
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
- Real TCP server speaking Redis/RESP on a configurable (or random) port.
- Supports multiple simultaneous connections and pipelining.
- Extensible command dispatcher, with built-in support for:
PING,ECHO,AUTH,HELLO,SELECT,QUIT,INFO,CLIENT,COMMANDGET,SET,DEL,EXISTS,EXPIRE,TTL,KEYS,SCAN,FLUSHALLHGET,HSET,HGETALL,HMGET,HMSETSADD,SMEMBERS,SREM,SISMEMBER,SSCANLPUSH,RPUSH,LPOP,RPOPMGET,MSET,INCR,DECRPUBLISH,SUBSCRIBE,UNSUBSCRIBE(simplified)
- Mock/stub engine:
-
Match by command name, arguments, regex, custom predicate (
{:fn, fun}), or connection id. -
Define:
- constant responses
-
callback functions (
{:fun, fn command -> response end}) -
delays (
{:delay, ms, response}), timeouts (:timeout) and missing replies (:no_reply) -
connection closes (
:closeor{:close, response}) -
protocol errors (
{:protocol_error, bytes}) and partial responses ({:partial, [chunks]})
- Sequential responses per rule.
- Full call history for assertions.
-
Match by command name, arguments, regex, custom predicate (
- Stateful in-memory store:
- Strings, hashes, sets, lists and TTLs (enough for most ejabberd/XMPP tests).
- ExUnit helper for quick integration in Elixir projects.
- Designed to be fast to start, good for parallel test runs.
Installation
Add to your mix.exs dependencies.
def deps do
[
{:faux_redis, "~> 1.0", only: :test}
]
endThen fetch dependencies:
mix deps.getBasic 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)
endUsing 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
endMocking API
The public API lives in the FauxRedis module:
start_link/1,child_spec/1stub/2(keyword list:matcher,respond,conn_id/conn_ids,max_calls) andstub/3(short form)expect/3calls/1reset!/1port/1,address/1(host is always"127.0.0.1"in the returned tuple; bind address is set via:ipinstart_link/1)stop/1
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: _
}
] = callsResetting between tests
FauxRedis.reset!(server)This clears:
- all keys and TTLs
- all mock rules and expectations
- call history
- auth and pub/sub state
Using with ejabberd / XMPP
FauxRedis is designed to play nicely with typical Redis clients used by ejabberd and other XMPP systems:
Start the server on a random port (the default when passing
port: 0).Ask FauxRedis for the address and use it in ejabberd config:
{:ok, server} = FauxRedis.start_link(port: 0) {"127.0.0.1", port} = FauxRedis.address(server)Then, in your
ejabberd.ymltest configuration (or its template):redis: server: "127.0.0.1" port: <PORT_FROM_FauxRedis.address/1>Typically you will inject the port via environment variable or a generated config file in your test harness.
Use the mocking API to simulate:
- normal key/value behaviour
-
authentication success/failure via
AUTH -
slow Redis (
{:delay, ms, inner}) -
completely unavailable Redis (
:timeout/:close) -
protocol-level weirdness (
{:protocol_error, bytes},{:partial, chunks})
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
FauxRedis.Application– starts aRegistryfor optional server naming.FauxRedis.Command– parsed command struct (name, args, connection id, DB, …).FauxRedis.Server– GenServer owning:- TCP listener
- connection registry
- in-memory store
- mock rules and call history
- minimal pub/sub state
FauxRedis.Connection– per-socket process:- buffers incoming bytes
- decodes RESP frames and pipelines
- applies response specifications (delays, closes, partials, etc.)
FauxRedis.RESP– RESP2 encoder/decoder.FauxRedis.Store– in-memory Redis-like store.FauxRedis.MockRule/MockEngine– rule representation and evaluation.FauxRedis.Case– ExUnit helper case template.
Limitations vs real Redis
FauxRedis is not a full Redis implementation. Notable limitations:
-
Only a subset of commands is implemented; behaviour of
SCAN,KEYS,SSCAN, etc. follows this implementation, not every edge case of Redis. -
Pub/sub semantics are simplified:
SUBSCRIBE/UNSUBSCRIBEdo not switch the connection into a dedicated pub/sub mode; they manage an internal subscription table and return simple acknowledgements.-
Messages are pushed as RESP arrays
["message", channel, payload], but not all edge cases of real Redis pub/sub are reproduced.
- Persistence, replication, clustering and scripting are not supported.
- Performance characteristics are tuned for tests, not for production loads.
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 testThe 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.