Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .claude/skills/test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,9 @@ real service in a throwaway container instead:
Rules: build the container **inside the test method** (no shared fixture — the isolation rule still
applies), pin the image to the tag `docker-compose.yml` runs, and `Assert.Inconclusive` when no
runtime is reachable *unless* `PROXYTRACE_REQUIRE_DOCKER_TESTS` is set — `dotnet test` must not
require Docker locally, while CI must not skip silently. Full rationale in
require Docker locally, while CI must not skip silently. The skip guard must wrap the builder's
`Build()` too, not only `StartAsync`: `Build()` pings the Docker endpoint, so that is where a
missing runtime actually throws. Full rationale in
[`docs/testing.md`](../../../docs/testing.md#container-backed-tests). Default to a mock; this is a
supplement for the cases a mock structurally cannot cover.

Expand Down
12 changes: 10 additions & 2 deletions Proxytrace.Messaging.Tests/RedisIngestionStreamIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,13 @@ public async Task ConsumeAsync_WithEntryPendingOnADeadConsumer_ReclaimsItViaAuto

private static async Task<RedisContainer> StartRedisAsync(CancellationToken cancellationToken)
{
RedisContainer container = new RedisBuilder(RedisImage).Build();
RedisContainer? container = null;
try
{
// Build() must stay inside the guard: it validates the builder by resolving and pinging
// the Docker endpoint, so on a machine without a runtime the throw happens here and
// never reaches StartAsync — a skip guard wrapping only the start never fires.
container = new RedisBuilder(RedisImage).Build();
await container.StartAsync(cancellationToken);
}
// Docker unavailable surfaces as anything from a socket-level HttpRequestException to a
Expand All @@ -142,7 +146,11 @@ private static async Task<RedisContainer> StartRedisAsync(CancellationToken canc
// runtime is guaranteed (CI), nothing is swallowed and the failure is reported as-is.
catch (Exception ex) when (!DockerRequired)
{
await container.DisposeAsync();
if (container is not null)
{
await container.DisposeAsync();
}

Assert.Inconclusive(
$"Skipping the real-Redis transport test — no usable container runtime: {ex.Message}");
}
Expand Down
5 changes: 5 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ Docker dependency. Setting **`PROXYTRACE_REQUIRE_DOCKER_TESTS=1|true`** inverts
failure is then a real failure. CI's `backend` job sets it (see [`ci.md`](ci.md)) so the coverage
can never be lost silently, which is the same class of false-green the tests exist to close.

The guard has to wrap the builder's `Build()` call, not just `StartAsync`: Testcontainers validates
a builder by resolving and pinging the Docker endpoint, so on a machine without a runtime the throw
happens at `Build()` and a `try` that starts one line later never sees it (#526). Construct the
container inside the guarded block and null-check it before disposing on the skip path.

Run them locally like any other test — with Docker up they just run:

```bash
Expand Down
Loading