SpoofDB: One Go binary that fakes Postgres, MySQL, ClickHouse, and Redis
Database testing often lands in an awkward middle ground. You either spin up real services and pay the startup cost, or you mock an interface and stop testing what your application actually sends over the wire. SpoofDB takes a more interesting route: one Go binary that accepts Postgres, MySQL, ClickHouse, and Redis clients, then serves test data without running those databases.
For the SQL protocols, SpoofDB puts a shared DuckDB engine behind the listeners. Redis is handled separately with an embedded miniredis server seeded from RDB dumps. The result is still the same practical idea: change the host and port, keep the client library you already use.
How it’s structured
The project layout is easy to follow because it mirrors the protocols it supports. Each database-facing piece has its own package under internal/:
internal/pg/— Postgres wire protocolinternal/mysql/— MySQL wire protocolinternal/clickhouse/— ClickHouse wire protocolinternal/redis/— embedded Redis servers and RDB seedinginternal/engine/— DuckDB-backed SQL engine, shared by the SQL protocols
The CLI entry point lives in cmd/spoofdb/main.go, with flag parsing and ad-hoc config construction in cmd/spoofdb/cli.go. You can run it with a config file, or pass repeatable -source and -spoof flags to say what data to load and which protocol listeners to expose.
That separation is the best part of the design. Postgres, MySQL, and ClickHouse each own their network protocol details, but the query execution path is shared. Redis stays out of that SQL path entirely, which matches how the tool actually works.
Wire protocol implementation in Go
This is where the project gets genuinely impressive. Implementing real database wire protocols is a pain.
Take Postgres. The Postgres wire protocol is binary, with specific messages for startup, authentication, parsing, binding, describing, executing, and returning rows. internal/pg/handler.go uses pgproto3, handles startup, supports simple and extended query flows, and sends row descriptions, data rows, notices, and command tags back to the client.
internal/mysql/handler.go uses go-mysql to act as a MySQL server. It accepts any username with an empty password, runs regular queries against DuckDB, supports prepared statement execution by substituting arguments, and exposes approximation warnings through SHOW WARNINGS.
ClickHouse is its own beast. internal/clickhouse/handler.go deals with the ClickHouse native protocol, and internal/clickhouse/columns.go maps column types between ClickHouse’s type system and DuckDB’s.
Redis breaks the pattern because it is not SQL. internal/redis/redis.go starts one miniredis instance per configured Redis endpoint, and internal/redis/seed.go replays keys from RDB files. Strings, lists, sets, hashes, and sorted sets are loaded; streams are explicitly skipped with a log notice.
Each protocol package also has a listener.go file (e.g., internal/pg/listener.go, internal/mysql/listener.go, internal/clickhouse/listener.go) that sets up the TCP listener using Go’s net package. Separating listener from handler is idiomatic Go for network servers, and it makes each component independently testable.
DuckDB as the universal query engine
All SQL queries, regardless of which protocol delivered them, land in internal/engine/duckdb.go. DuckDB is an embedded analytical database — think SQLite-shaped deployment with a columnar execution engine. SpoofDB uses it through the go-duckdb driver, so the engine code still looks like normal Go database/sql.
This is a sensible architectural call. SpoofDB is not trying to implement separate Postgres, MySQL, and ClickHouse query engines. It loads SQL-shaped sources into DuckDB, then puts protocol adapters in front of the same engine. File sources can be CSV or Parquet; live Postgres, MySQL, and ClickHouse sources are snapshotted into DuckDB at startup.
The code also has a useful persistence model. By default, runtime-created SQL tables and rows live in a DuckDB file, while file sources are registered again on startup. Redis data is different: it is loaded into miniredis memory from an RDB dump and resets when the process restarts.
Fidelity: how close is close enough?
Multiple packages have fidelity.go files, and this design decision caught my eye:
internal/fidelity/fidelity.go— shared fidelity definitionsinternal/pg/fidelity.go— Postgres-specific fidelity rulesinternal/mysql/fidelity.go— MySQL-specific fidelity rulesinternal/clickhouse/fidelity.go— ClickHouse-specific fidelity rules
“Fidelity” here means how openly SpoofDB reports places where the fake cannot be bit-for-bit identical to the real database. The project does not hide those approximations. Postgres emits NoticeResponse warnings, MySQL reports a warning count and supports SHOW WARNINGS, and ClickHouse can send server log packets.
That is the right kind of tradeoff for a testing tool. For example, the README calls out ClickHouse limitations around nullable columns, arrays, tuples, maps, date ranges, and decimal precision. Those gaps matter, but surfacing them is much better than silently pretending the fake is exact.
Where this fits in Go testing
The standard Go approach for database code is interface-based mocking. Define a Repository interface, write a mock, test your business logic against it. That works fine for unit tests.
SpoofDB fills a different gap: integration tests where you want to verify the SQL your app actually sends, the statements your ORM generates, and the basic client behavior around connections and prepared statements. Because it speaks the real client protocols, your application can use its normal drivers.
Configuration is defined in internal/config/config.go and normally lives in config/spoofdb.huml. The config separates sources from spoof targets, which is a nice model: a Parquet file, CSV directory, or live database snapshot can be served over one or more SQL protocols.
CLI design
The CLI is intentionally small. main.go loads config, creates the engines, starts the listeners, and waits for shutdown. cli.go handles the convenience path: infer a source from a directory, .csv, .parquet, .rdb, or database DSN, then map postgres, mysql, clickhouse, or redis spoof targets to their default or supplied ports.
What it won’t do
DuckDB is an analytical engine, and SpoofDB runs SQL through DuckDB’s dialect. If your tests depend on Postgres or MySQL-specific transactional semantics, locking behavior, stored procedures, or every last dialect feature, you still need the real database somewhere in the test pyramid.
ClickHouse clients currently need compression disabled, and the README is clear that ClickHouse serving has type-system limitations. Redis streams are skipped when loading RDB data. Those are reasonable constraints, but they are constraints you should know before swapping it into CI.
When it makes sense
SpoofDB is for running integration tests without managing a full stack of database containers. Build the binary with CGO enabled, start it with a HUML config or -source/-spoof flags, point your normal clients at it, run the tests, and shut it down.
For unit tests, use interface mocks. For full end-to-end validation, use real databases. SpoofDB sits in the middle: much closer to the wire than a mock, much lighter than running Postgres, MySQL, ClickHouse, and Redis just to exercise everyday application queries.