docs/integration-testing.md
This document describes how Jedis integration tests are wired up: the Redis test environment, how it is started, how tests discover their servers, and how this maps to CI.
Tests do not start Redis. They connect to servers that are already running. The
servers are defined by src/test/resources/env/docker-compose.yml, started via
make start (Makefile), and described to the tests by
src/test/resources/endpoints.json. A test selects a server by name through
Endpoints.getRedisEndpoint("<name>")
(src/test/java/redis/clients/jedis/Endpoints.java).
make start version=8.6 && mvn clean verify && make stop.make start version=8.6 then mvn -Dtest=YourIT verify
(don't forget make stop).src/test/resources/env/docker-compose.yml
(set REDIS_CLUSTER/TLS_ENABLED/TLS_CLIENT_CNS/ports/volumes), then add a
matching named entry to src/test/resources/endpoints.json.Endpoints.getRedisEndpoint("<name>")
(src/test/java/redis/clients/jedis/Endpoints.java) →
endpoint.getClientConfigBuilder().build().endpoint.getCertificatesLocation()
via TlsUtil (src/test/java/redis/clients/jedis/util/TlsUtil.java); certs
come from <TEST_WORK_FOLDER>/<env>/work/tls/.src/test/resources/endpoints.json for oss-docker,
src/test/resources/endpoints_source.json for oss-source) — you're probably
on the oss-source (local) provider.All Redis servers used in tests run inside the redislabs/client-libs-test
Docker image, published publicly on
Docker Hub. The same image
is used across several Redis client libraries (Jedis, Lettuce, redis-py,
node-redis, go-redis, …).
The image wraps a chosen Redis version and bootstraps a standalone,
replication + sentinel, or cluster topology — optionally with
TLS / mTLS and modules — entirely from environment variables. Tags follow the
Redis version (e.g. redislabs/client-libs-test:8.6). It is not for
production use.
You normally never run it by hand — make start brings up the whole topology via
Docker Compose (see §4). But the image is self-contained, so you can launch a
single instance directly to experiment:
# standalone on :6379
docker run --rm -p 6379:6379 redislabs/client-libs-test:8.6
# 3-node cluster on :7000-7002
docker run --rm -p 7000-7002:7000-7002 \
-e REDIS_CLUSTER=yes -e NODES=3 -e PORT=7000 redislabs/client-libs-test:8.6
| Concern | How it works |
|---|---|
| Base image | redis:<tag> (OSS) or redis/redis-stack:<tag> (with modules), selectable at build time via BASE_IMAGE / BASE_IMAGE_TAG. |
| Topology | REDIS_CLUSTER=yes → cluster (auto-bootstrapped with redis-cli --cluster create); otherwise standalone/replicated. NODES=N sets node count (cluster default 3, standalone 1). REPLICAS=N sets replicas per cluster master. |
| Ports | Non-TLS nodes: PORT, PORT+1, … (default PORT=3000). TLS nodes: TLS_PORT, TLS_PORT+1, … (default TLS_PORT=4430). |
| Auth | REDIS_PASSWORD → requirepass+masterauth. REDIS_CLIENT_USER/REDIS_CLIENT_PASSWORD are the credentials the container itself uses to create/check the cluster. |
| Protected mode | PROTECTED_MODE=yes sets Redis protected-mode (default no). |
| TLS | TLS_ENABLED=yes auto-generates a self-signed CA, a server cert, and client certs into /redis/work/tls/, and enables a TLS port per node. |
| mTLS | TLS_CLIENT_CNS="cn1 cn2 …" generates one client cert (+ .p12) per CN. TLS_AUTH_CLIENTS_USER=CN maps the cert CN to a Redis ACL user (Redis ≥ 8.6); off disables client-cert auth. |
| Modules | When the base image ships modules (e.g. redis/redis-stack:<tag>), they are auto-loaded. redislabs/client-libs-test has matching …-stack style tags for module testing. |
| Extra directives | Any trailing args become redis-server flags (e.g. --maxmemory 256mb). A few are container-managed and cannot be overridden: --port, --dir, --logfile, --pidfile, --cluster-enabled, --cluster-config-file, --protected-mode. |
| Mount | Purpose |
|---|---|
-v <host>/config:/redis/config:r | Input. Pre-baked per-node config. Node dirs must be named node-<port>[-<tlsport>]; a sentinel node dir must be named node-sentinel-<...>. Each may contain redis.conf and (for cluster) nodes.conf. Optional tls/ subdir supplies pre-generated certificates. |
-v <host>/work:/redis/work:rw | Output. Runtime state: node-<i>/{redis.conf,redis.log,redis.pid,nodes.conf} and generated tls/ (ca.crt, ca.key, redis.crt, redis.key, <cn>.p12, …). Tests read TLS certs from here. |
When TLS_ENABLED=yes, the container writes these files to its
/redis/work/tls/ directory (surfaced on the host under the work/ mount — see
§1.2 — which is where the tests read them from):
| File | What it is |
|---|---|
ca.crt, ca.key | Self-signed test CA (signs everything else). |
redis.crt, redis.key | Server certificate (CN localhost). |
<cn>.crt, <cn>.key, <cn>.p12 | One client cert per name in TLS_CLIENT_CNS, for mTLS. |
The PKCS#12 keystores (*.p12) use the password changeit. Certs are
generated once and reused on container restart.
An endpoint is a host:port pair for one Redis node (127.0.0.1:6379
non-TLS, 127.0.0.1:6390 TLS). For cluster, every node is an endpoint and the
client discovers the rest via CLUSTER NODES.
Everything lives under src/test/resources/env/.
src/test/resources/env/
├── docker-compose.yml # all Redis services (standalone/sentinel/cluster/tls/mtls/stack)
├── .env # base vars (default REDIS_VERSION, image name, work dir)
├── .env.v6.2 … .env.v8.10 # per-version overrides (just pin REDIS_VERSION)
├── redis1-2-5-8-sentinel/ # one dir per environment
│ ├── config/node-<port>/redis.conf # mounted read-only into the container
│ └── work/ # runtime state (TLS certs land in work/tls/)
├── standalone2-sentinel/
├── cluster-unbound/
├── ...
Key .env values:
REDIS_VERSION=8.6.1
CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test
REDIS_ENV_CONF_DIR=./ # base for the :/redis/config mounts
REDIS_ENV_WORK_DIR=/tmp/redis-env-work # base for the :/redis/work mounts
Each docker-compose.yml service is one client-libs-test container, e.g.:
redis1-2-5-8-sentinel:
<<: *client-libs-image # image: ${CLIENT_LIBS_TEST_IMAGE}:${REDIS_VERSION}
environment: [REDIS_CLUSTER=no, REDIS_CLIENT_USER=deploy,
REDIS_CLIENT_PASSWORD=verify, TLS_ENABLED=yes]
ports: ["6379:6379", "6390:6390", "26379:26379", "36379:36379"]
volumes:
- ${REDIS_ENV_CONF_DIR}/redis1-2-5-8-sentinel/config:/redis/config:r
- ${REDIS_ENV_WORK_DIR}/redis1-2-5-8-sentinel/work:/redis/work:rw
The compose file provides the full matrix: standalone (+TLS/+ACL), sentinel,
cluster-unbound, cluster-stable (+TLS), standalone-mtls, cluster-mtls,
a Redis-Stack service, Toxiproxy (network fault injection), and an explicitly
unavailable instance for failover tests.
endpoints.jsonTests never hardcode hosts. They look up a named entry in
src/test/resources/endpoints.json, loaded by Endpoints.getRedisEndpoint(name).
{
"standalone0-tls": {
"endpoints": ["rediss://localhost:6390"], // one URI (standalone) or many (cluster)
"tls": true, // required
"username": "default", // optional ACL user
"password": "foobared", // optional
"tls_cert_path": "redis1-2-5-8-sentinel/work/tls", // optional; resolved under TEST_WORK_FOLDER
"bdbId": 0 // optional (Redis Enterprise only)
}
}
redis:// = plaintext, rediss:// = TLS. Names encode the topology and mode,
e.g. standalone0, standalone0-acl, standalone0-tls,
standalone0-tls-wronghost (CN-mismatch negative test), cluster-stable,
cluster-stable-tls, cluster-mtls, sentinel-standalone0.
src/test/java/redis/clients/jedis/)| Class | Role |
|---|---|
Endpoints | Loads the JSON at class-init; getRedisEndpoint(name) returns an EndpointConfig (or throws TestAbortedException → test skipped, not failed, when the endpoint is absent). |
EndpointConfig | Parsed entry (Gson, LOWER_CASE_WITH_UNDERSCORES). Provides getHostAndPort(), getHostsAndPorts(), getPassword(), isTls(), getCertificatesLocation(), and getClientConfigBuilder() (pre-wired credentials + SSL socket factory). |
util/TestEnvUtil | Chooses which endpoints file to load (see §6). |
util/TlsUtil | Builds a JCEKS truststore from the tls_cert_path cert dir(s) and sets javax.net.ssl.trustStore* system properties. |
Typical usage:
@BeforeAll static void setUp() {
endpoint = Endpoints.getRedisEndpoint("standalone0-tls");
}
// ...
try (Jedis j = new Jedis(endpoint.getHostAndPort(), endpoint.getClientConfigBuilder().build())) { ... }
mTLS / TLS tests build a truststore from the cert location(s):
trustStorePath = TlsUtil.createAndSaveTestTruststore(name, List.of(endpoint.getCertificatesLocation()), "changeit");
TlsUtil.setCustomTrustStore(trustStorePath, "changeit");
| Command | Effect |
|---|---|
make start version=8.6 | docker compose … up -d --wait with .env + .env.v8.6. Supported: 6.2, 7.2, 7.4, 8.0, 8.2, 8.4, 8.6, 8.8, 8.10 (default 8.10). |
make stop | docker compose … down. |
make test [version=…] | start → mvn clean verify → stop. |
mvn clean verify | Run tests against an already-started env (use this from the IDE between make start/make stop). |
Custom image instead of a version: make start CLIENT_LIBS_TEST_IMAGE_TAG=<tag>.
pom.xml)| Group | Tag / naming | Runner | Default |
|---|---|---|---|
| Unit | @Tag("unit"), no network | Surefire | run (skipUnitTests=false) |
| Integration | *IT (new) — legacy *IntegrationTest / @Tag("integration") still run | Failsafe | run (skipIntegrationTests=false) |
| Scenario | @Tag("scenario") | Failsafe | skipped (skipScenarioTests=true) |
New integration tests must be named *IT — do not use *IntegrationTest or
the @Tag("integration") marker. See §5.
Useful flags: -DskipUnitTests=true (CI Docker job runs integration only),
-Dtest=<Class#method> to narrow.
All test code lives under src/test/java/redis/clients/jedis/. The package
root holds the broad client/pool/connection tests; everything else is grouped by
area:
src/test/java/redis/clients/jedis/
├── commands/ # command coverage (integration)
│ ├── jedis/ # against the legacy Jedis API
│ ├── unified/ # against the UnifiedJedis surface
│ └── commandobjects/
├── search/ json/ timeseries/ # module command tests (integration)
├── tls/ # TLS / mTLS / SSL options (integration, *IT.java)
├── csc/ # client-side caching (integration)
├── failover/ # multi-db / failover (integration, @Tag("failover"))
├── authentication/ # auth + token-based auth
├── mcf/ providers/ executors/ prefix/ # connection/provider internals
├── params/ builders/ resps/ # mostly unit (arg/response objects)
├── mocked/ # pure unit, Mockito-based — no server
├── scenario/ # @Tag("scenario") — long-running, skipped by default
├── examples/ # io.redis.examples — excluded from test runs
├── benchmark/ codegen/ # excluded from test runs
└── util/ # shared test helpers (TestEnvUtil, TlsUtil, …) — not tests
What decides whether a test is run by Surefire (unit) or Failsafe (integration)
is how the test is marked — its file-name suffix and/or JUnit tag — not which
folder it sits in (the runners select by suffix and tag — see §4 and pom.xml).
New integration tests MUST be named *IT. This is the single accepted
convention for new code. Failsafe still recognises the older *IntegrationTest
name and the @Tag("integration") marker so the many existing classes keep
running while they are migrated, but do not use them for new tests — name
your class FooIT and nothing else.
| Test needs… | Name / mark it as… | Runner |
|---|---|---|
| No server (pure logic, mocks) | a plain *Test / *Tests class, untagged | Surefire |
| A running Redis | *IT file name | Failsafe |
| A long-running scenario | @Tag("scenario"), placed under scenario/ | Failsafe (off by default) |
Guidance for new tests:
FooIT.java (the only accepted name
for new integration tests — not *IntegrationTest, and never
@Tag("integration")). Put it in the area package it exercises (commands/…,
search/, tls/, etc.). It connects to a server resolved through
Endpoints.getRedisEndpoint(...).mocked/ if it is
Mockito-based), name it FooTest, and leave it untagged. It must pass with no
Redis running.examples/, benchmark/, or
codegen/ — those packages are excluded from the test run. Shared helpers go
in util/ (helpers there are not collected as tests even if named *Test).Selection is via the TEST_ENV_PROVIDER env var, read by TestEnvUtil:
| Provider | Endpoints file | Servers | Use |
|---|---|---|---|
oss-docker (default) | endpoints.json | Full compose matrix (standalone, sentinel, cluster, TLS, mTLS, stack) | Full integration coverage. |
oss-source | endpoints_source.json | 3 plain redis-server processes started by make start-local: standalone0 (6379, ACL users), a Unix-socket instance, and one deliberately-unavailable instance | Lightweight local dev; limited subset. |
re | (REDIS_ENDPOINTS_CONFIG_PATH) | Redis Enterprise | Internal. |
Override the endpoints file directly with REDIS_ENDPOINTS_CONFIG_PATH, and the
TLS cert root with TEST_WORK_FOLDER (default /tmp/redis-env-work).
make system-setup # CI only: build Redis from github.com/redis/redis + the test module (testmodule.so)
make test-local # = start-local (TEST_ENV_PROVIDER=oss-source) → mvn verify → stop-local
make start-local runs three local redis-server instances and make mvn-test-local forces TEST_ENV_PROVIDER=oss-source, so tests load
endpoints_source.json. Endpoints absent there (cluster, sentinel, TLS, mTLS,
stack) resolve to a TestAbortedException and are skipped — that is why the
local env runs only a subset. Full coverage requires the Docker env.
.github/workflows/)| Workflow | Environment | What it does |
|---|---|---|
test-on-docker.yml | Docker (full) | Matrix over Redis 7.2 → 8.10. Uses the run-tests composite action: make start version=<v> → TEST_ENV_PROVIDER=oss-docker, TEST_WORK_FOLDER=$REDIS_ENV_WORK_DIR, mvn -B -DskipUnitTests=true clean compile verify → make stop. Uploads JaCoCo + Surefire/Failsafe reports. |
integration.yml | Local-from-source (limited) | Java 8; make system-setup builds Redis from source + the test module; make test-local runs the oss-source subset. |
Both clients pin Java 8 for the test build. Use it when reproducing CI.