Skip to main content

Container test fixtures

Respire.Testing.Containers starts disposable Redis or Valkey servers in Docker for integration tests. It works with any test framework (TUnit, xUnit, NUnit, or none).

Requirements:

  • A local Docker engine with Linux containers. Remote engines are not supported.
  • The normal .NET runtime. The package depends on Testcontainers and is not Native AOT compatible; keep it in test projects.
using Respire.Testing.Containers;

await using var fixture = await RespireContainerFixture.StartAsync();
await using var client = await RespireClient.ConnectAsync(fixture.CreateOptions());
await client.SetAsync("example", "value");
var value = await client.GetStringAsync("example");
if (value != "value") throw new InvalidOperationException("Round trip failed.");

Dispose clients before their fixture. Disposal removes only the fixture's own container. Each fixture has its own data directory and never reuses containers or volumes. Failed or cancelled startup cleans up before returning the error.

Topologies and versions​

using Respire.Testing.Containers;

await using var fixture = await RespireContainerFixture.StartAsync(new()
{
Server = RespireContainerServer.Valkey,
Topology = RespireContainerTopology.Cluster,
Image = "valkey/valkey:8.1-alpine",
StartupTimeout = TimeSpan.FromMinutes(2)
});
await using var client = await RespireClient.ConnectAsync(
fixture.CreateOptions() with { Protocol = RespireProtocol.Resp3 });
await client.SetAsync("{customer}:name", "Ada");
TopologyProcessesClient configuration
StandaloneOne serverOne mapped endpoint
ClusterThree primaries covering all 16,384 slots, no replicasAll three seeds, UseCluster = true
SentinelOne primary, one replica, three Sentinels with quorum twoThree Sentinel endpoints and service name respire-test
OptionDefaultMeaning
ServerRedisRespireContainerServer.Redis or Valkey.
Imageredis:7.2-alpine / valkey/valkey:8.1-alpinePin a tag or digest for reproducible runs. The image needs /bin/sh, mkdir, tail, and the server and CLI binaries. Cluster needs Redis 7+ or Valkey (CLUSTER ADDSLOTSRANGE).
TopologyStandaloneSee the table above.
StartupTimeout2 minutesOne deadline for image pull, retries, and readiness.
SentinelDownAfter5 secondsSentinel down-detection interval.
SentinelFailoverTimeout10 secondsSentinel failover timeout. Sentinels retry an aborted election after twice this value.

CreateOptions() returns fresh options each call. Use a with expression to set protocol, timeouts, client name, or admin access. DataEndpoints, SentinelEndpoints, and ContainerId help with diagnostics and direct node connections.

Sentinel failover tests​

  • The first data endpoint is the initial primary; it is not updated after failover. Sentinel discovery finds the current primary at connect time.
  • The fixture does not add automatic failover handling to the client.
  • StopDataNodeAsync(index), StartDataNodeAsync(index), and WaitForDataNodeReplicaAsync(index) stop a node, restart it, and wait until it reports as a replica. Index 0 is the initial primary, 1 the initial replica. On other topologies they throw InvalidOperationException.
  • Shorter Sentinel timeouts speed up failover tests but can cause false failovers on a busy host. Keep the defaults for tests that do not fail over.
using Respire.Testing.Containers;

await using var fixture = await RespireContainerFixture.StartAsync(new()
{
Topology = RespireContainerTopology.Sentinel,
SentinelDownAfter = TimeSpan.FromSeconds(3),
SentinelFailoverTimeout = TimeSpan.FromSeconds(3)
});

Docker networking and limits​

  • Ports are published only on 127.0.0.1. Standalone uses Docker-assigned random ports. Cluster and Sentinel use the same port inside and outside the container, so advertised addresses work (Sentinel and Docker NAT). Cluster bus ports are not published.
  • Ports come from the OS ephemeral range. Other fixtures and services are never stopped.
  • If Docker reports a host-port collision ("port already allocated" or "address already in use"), the fixture removes the container and retries up to twice. Other errors (permissions, reserved ports, images, authentication, readiness) are not retried.
  • The local-engine check runs after startup, so a remote engine can pull and start the container before the fixture rejects and removes it.
  • All processes share one container. Use real deployments to test machine failure, network partitions, durability, or high availability.
  • Servers are unauthenticated. Do not store sensitive data in them.

Startup and diagnostics​

Startup waits until the topology is ready: PING for standalone, full slot coverage for Cluster, or replication plus Sentinel quorum and replica discovery for Sentinel. StartupTimeout covers the whole startup and is not reset for retries. A timeout error names the stage and last readiness reply.

On failure:

  • The final Docker exception is thrown. Its Data includes RespireFixture.StartupAttempt, RespireFixture.SelectedPorts, RespireFixture.ContainerId, and RespireFixture.PreviousPortCollisions.
  • If cleanup also fails, all causes are thrown together in an AggregateException.
  • Cancellation and timeout exceptions keep the original error as the inner exception.
  • If the container started, the last 4 KiB of each daemon log is stored in Data["RespireFixture.DaemonLogs"] (at most 32,768 characters total) and written to standard error. Log collection is best effort and never hides the startup error.

The in-memory fake runs deterministic tests without Docker. The shared sample runs the same scenarios against the fake, Redis, and Valkey.