From 2c9c4069cc8857bedd5d8de780d6dc3d8df729ad Mon Sep 17 00:00:00 2001 From: Havret Date: Sun, 3 May 2026 10:30:32 +0200 Subject: [PATCH 1/3] Add integration test style guide with conventions and best practices --- .../skills/integration-test-style/SKILL.md | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 .claude/skills/integration-test-style/SKILL.md diff --git a/.claude/skills/integration-test-style/SKILL.md b/.claude/skills/integration-test-style/SKILL.md new file mode 100644 index 0000000..27535a9 --- /dev/null +++ b/.claude/skills/integration-test-style/SKILL.md @@ -0,0 +1,213 @@ +--- +name: integration-test-style +description: Guidelines and conventions for writing integration tests in this repo. Use when creating, reviewing, or discussing integration tests against the real Artemis broker. +--- + +# Integration Test Style Guide + +This skill describes the required style for all integration tests in this repository. Follow it exactly when writing new tests or reviewing existing ones. + +--- + +## File & Class Naming + +- Files use the `*Spec.cs` suffix (BDD-style), e.g. `MessageAcknowledgementSpec.cs` +- Core tests live in `test/ArtemisNetClient.IntegrationTests/` +- Extension tests live in their own `*.IntegrationTests/` project +- Subtopics go in subdirectories (e.g. `TopologyManagement/FilterExpressionsSpec.cs`) + +--- + +## Base Class + +All core integration test classes inherit from `ActiveMQNetIntegrationSpec`: + +```csharp +public class MyFeatureSpec : ActiveMQNetIntegrationSpec +{ + public MyFeatureSpec(ITestOutputHelper output) : base(output) { } +} +``` + +The base class provides: +- `CreateConnection()` — returns an `IConnection` backed by a `ConnectionFactory` wired to `XUnitLoggerFactory` with GUID message IDs +- `CancellationToken` — 1 minute in DEBUG, 10 seconds in Release +- `GetEndpoint()` — resolves broker address from env vars (`ARTEMIS_HOST`, `ARTEMIS_PORT`, `ARTEMIS_USERNAME`, `ARTEMIS_PASSWORD`; all with sensible defaults) + +--- + +## Test Method Conventions + +- Every test is `async Task` — no synchronous tests +- Use `[Fact]` only — no `[Theory]` +- Method names read as sentences: `Should_acknowledge_message`, `Should_send_message_with_priority` + +```csharp +[Fact] +public async Task Should_do_something_meaningful() +{ + // ... +} +``` + +--- + +## Arrange-Act-Assert Structure + +Tests follow a clear, unlabelled AAA structure. Keep each section visually separate with a blank line. + +```csharp +[Fact] +public async Task Should_acknowledge_message() +{ + await using var connection = await CreateConnection(); + var address = Guid.NewGuid().ToString(); + await using var producer = await connection.CreateAnonymousProducerAsync(CancellationToken); + await using var consumer = await connection.CreateConsumerAsync(address, RoutingType.Anycast, CancellationToken); + + await producer.SendAsync(address, RoutingType.Anycast, new Message("foo"), CancellationToken); + var msg = await consumer.ReceiveAsync(CancellationToken); + await consumer.AcceptAsync(msg); + + await consumer.DisposeAsync(); + var consumer2 = await connection.CreateConsumerAsync(address, RoutingType.Anycast, CancellationToken); + await Assert.ThrowsAsync( + async () => await consumer2.ReceiveAsync( + new CancellationTokenSource(TimeSpan.FromMilliseconds(500)).Token)); +} +``` + +--- + +## Resource Management + +- Declare all disposables with `await using var` so they are cleaned up automatically even when a test fails +- When you need to dispose mid-test (to verify post-disposal state), call `await x.DisposeAsync()` explicitly at that point — do not declare with `await using` in that case + +--- + +## Test Isolation + +Use `Guid.NewGuid().ToString()` for every address, queue name, group ID, or any other broker resource name. Never hardcode strings that could collide across parallel test runs. + +```csharp +var address = Guid.NewGuid().ToString(); +var queue = Guid.NewGuid().ToString(); +``` + +--- + +## Cancellation & Negative Assertions + +Use `CancellationToken` (from the base class) for all normal receive calls. For negative assertions (verifying that a message does *not* arrive), use a short inline token: + +```csharp +await Assert.ThrowsAsync( + async () => await consumer.ReceiveAsync( + new CancellationTokenSource(TimeSpan.FromMilliseconds(500)).Token)); +``` + +--- + +## Assertions + +Use plain xUnit assertions — no custom extensions, no FluentAssertions: + +```csharp +Assert.Equal("expected", actual); +Assert.NotNull(value); +Assert.Single(collection); +Assert.All(collection, item => Assert.Equal("x", item)); +await Assert.ThrowsAsync(...); +await Assert.ThrowsAnyAsync(...); +``` + +--- + +## Private Helper Methods + +Extract repeated sequences into `private static async Task` helpers within the same class: + +```csharp +private static async Task> ReceiveMessages( + IConsumer consumer, int count) +{ + var messages = new List(); + for (int i = 0; i < count; i++) + { + var msg = await consumer.ReceiveAsync(CancellationToken); + await consumer.AcceptAsync(msg); + messages.Add(msg); + } + return messages; +} +``` + +--- + +## DI / Hosting Extension Tests + +Tests for `ArtemisNetClient.Extensions.DependencyInjection` or `Hosting` do **not** use `ActiveMQNetIntegrationSpec`. Instead they use the local `TestFixture`: + +```csharp +public class ProducerSpec +{ + private readonly ITestOutputHelper _testOutputHelper; + + public ProducerSpec(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + [Fact] + public async Task Should_register_producer() + { + var address = Guid.NewGuid().ToString(); + + await using var fixture = await TestFixture.CreateAsync( + _testOutputHelper, + builder => builder.AddProducer(address, RoutingType.Anycast)); + + var producer = fixture.Services.GetRequiredService(); + Assert.NotNull(producer); + } +} +``` + +`TestFixture` wraps an `IHost`, exposes `Services`, `Connection`, and `CancellationToken`, and implements `IAsyncDisposable`. + +--- + +## Multi-Step Scenario Tests + +For complex flows with several distinct steps, use `NScenario` with `XUnitOutputAdapter` to produce readable output: + +```csharp +var scenario = TestScenarioFactory.Default(new XUnitOutputAdapter(_testOutputHelper)); + +var fixture = await scenario.Step("Set up consumers", async () => + await TestFixture.CreateAsync(_testOutputHelper, builder => + builder.AddSharedDurableConsumer(address, queue, ...))); + +await scenario.Step("Send messages", async () => { ... }); +await scenario.Step("Verify distribution", async () => { ... }); +``` + +Only use `NScenario` when a test has three or more meaningful named steps; simpler tests use plain AAA. + +--- + +## Summary Checklist + +When writing a new integration test, verify: + +- [ ] Class is in the correct `*.IntegrationTests` project +- [ ] File is named `*Spec.cs` +- [ ] Class inherits `ActiveMQNetIntegrationSpec` (or uses `TestFixture` for DI tests) +- [ ] Constructor injects `ITestOutputHelper` and passes it to `base(output)` +- [ ] All test methods are `async Task` with `[Fact]` +- [ ] All broker resource names use `Guid.NewGuid().ToString()` +- [ ] All disposables use `await using var` +- [ ] `CancellationToken` from the base class is passed to every receive call +- [ ] Negative assertions use a short `CancellationTokenSource` timeout +- [ ] Assertions use plain xUnit — no third-party assertion libraries From d3b7f368bb71f6d5644396ba700e48e8317944c9 Mon Sep 17 00:00:00 2001 From: Havret Date: Sun, 3 May 2026 10:35:41 +0200 Subject: [PATCH 2/3] Add unit test style guide with conventions and best practices --- .claude/skills/unit-test-style/SKILL.md | 348 ++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 .claude/skills/unit-test-style/SKILL.md diff --git a/.claude/skills/unit-test-style/SKILL.md b/.claude/skills/unit-test-style/SKILL.md new file mode 100644 index 0000000..032d3d8 --- /dev/null +++ b/.claude/skills/unit-test-style/SKILL.md @@ -0,0 +1,348 @@ +--- +name: unit-test-style +description: Guidelines and conventions for writing unit tests in this repo. Use when creating, reviewing, or discussing unit tests that run without a real broker. +--- + +# Unit Test Style Guide + +This skill describes the required style for all unit tests in this repository. Follow it exactly when writing new tests or reviewing existing ones. + +--- + +## Two Kinds of Unit Tests + +This repo has two flavours of unit test, each with its own infrastructure: + +| Kind | Projects | Infrastructure | +|---|---|---| +| **Core unit tests** | `test/ArtemisNetClient.UnitTests/` | `ActiveMQNetSpec` base class + `TestContainerHost` (in-process AMQP server) | +| **TestKit unit tests** | `test/ArtemisNetClient.Testing.UnitTests/` | `TestKit` (the in-process broker from the test kit library), no base class | +| **Pure model tests** | either project | No infrastructure — pure C# assertions | + +Choose the infrastructure that matches what you're testing: +- Testing AMQP protocol behaviour, connection/producer/consumer lifecycle → **Core unit test** +- Testing `TestKit` itself → **TestKit unit test** +- Testing data types, factory methods, or pure logic → **Pure model test** + +--- + +## File & Class Naming + +- Files use the `*Spec.cs` suffix (BDD-style), e.g. `ConsumerReceiveMessageSpec.cs` +- Subtopics go in subdirectories, e.g. `AutoRecovering/AutoRecoveringProducerSpec.cs` + +--- + +## Core Unit Tests — Base Class + +All core unit test classes inherit from `ActiveMQNetSpec`: + +```csharp +public class MyFeatureSpec : ActiveMQNetSpec +{ + public MyFeatureSpec(ITestOutputHelper output) : base(output) { } +} +``` + +The base class provides: + +| Member | What it gives you | +|---|---| +| `GetUniqueEndpoint()` | Unique local endpoint via `EndpointUtil` | +| `CreateConnection(endpoint)` | `IConnection` via `ConnectionFactory` with logging and GUID message IDs | +| `CreateConnectionWithoutAutomaticRecovery(endpoint)` | Same but with auto-recovery disabled | +| `CreateConnectionFactory()` | Configured `ConnectionFactory` for customisation | +| `CreateTestLoggerFactory()` | `XUnitLoggerFactory` wired to `ITestOutputHelper` | +| `CreateOpenedContainerHost(endpoint?, handler?)` | Running in-process AMQP server | +| `CreateContainerHost(endpoint?, handler?)` | Same but not yet opened | +| `CreateContainerHostThatWillNeverSendAttachFrameBack(endpoint)` | Host that ignores link attachment | +| `DisposeHostAndWaitUntilConnectionNotified(host, conn)` | Drops the host and waits for the connection close event | +| `WaitUntilConnectionRecovered(conn)` | Waits for the connection recovered event | +| `Timeout` | 1 minute (DEBUG) / 10 seconds (Release) | +| `ShortTimeout` | 100 ms — use for negative assertions | +| `CancellationToken` | Token backed by `Timeout` | + +--- + +## TestKit Unit Tests — No Base Class + +TestKit specs do not inherit from `ActiveMQNetSpec`. Set up AMQP tracing in the constructor and use `EndpointUtil` directly: + +```csharp +public class MyTestKitSpec +{ + public MyTestKitSpec(ITestOutputHelper output) + { + Trace.TraceLevel = TraceLevel.Information; + var logger = new XUnitLogger(output, "logger"); + Trace.TraceListener += (_, format, args) => logger.LogTrace(format, args); + } + + [Fact] + public async Task Should_do_something() + { + var endpoint = EndpointUtil.GetUniqueEndpoint(); + using var testKit = new TestKit(endpoint); + + var connectionFactory = new ConnectionFactory(); + await using var connection = await connectionFactory.CreateAsync(endpoint); + // ... + } +} +``` + +--- + +## Pure Model Tests — No Infrastructure + +For tests that only exercise data types, factory methods, or pure logic, omit both the base class and `ITestOutputHelper`: + +```csharp +public class EndpointSpec +{ + [Fact] + public void Should_create_endpoint() + { + var endpoint = Endpoint.Create("localhost", 5672, "guest", "guest"); + Assert.Equal("localhost", endpoint.Host); + } +} +``` + +Synchronous `[Fact]` tests are fine here — only use `async Task` when you're actually doing async I/O. + +--- + +## Test Method Conventions + +- Core and TestKit tests are `async Task` — no synchronous tests unless there is no async I/O at all +- Method names read as sentences: `Should_receive_message`, `Throws_when_invalid_scheme_specified` +- Both `[Fact]` and `[Theory]` are allowed; use `[Theory]` with `[MemberData]` or `[InlineData]` when the same behaviour must be verified across a set of inputs + +```csharp +[Fact] +public async Task Should_do_something_meaningful() +{ + // ... +} + +[Theory, MemberData(nameof(RoutingTypesData))] +public async Task Should_behave_differently_per_routing_type(RoutingType routingType, object expected) +{ + // ... +} +``` + +--- + +## In-Process AMQP Infrastructure + +Use the helpers from `ActiveMQNetSpec` to build a local AMQP server: + +```csharp +// Simple server — no custom handler +using var host = CreateOpenedContainerHost(endpoint); + +// Intercept AMQP frames +var testHandler = new TestHandler(@event => +{ + switch (@event.Id) + { + case EventId.ConnectionRemoteOpen: + // ... + break; + case EventId.LinkRemoteOpen when @event.Context is Attach attach && !attach.Role: + // ... + break; + } +}); +using var host = CreateOpenedContainerHost(endpoint, testHandler); + +// Enqueue messages for a consumer to pick up +var messageSource = host.CreateMessageSource("my-address"); +messageSource.Enqueue(new Message("foo")); + +// Intercept messages sent by a producer — synchronous dequeue +var messageProcessor = host.CreateMessageProcessor("my-address"); +// ...send a message... +var received = messageProcessor.Dequeue(Timeout); + +// Low-level link control +var linkProcessor = host.CreateTestLinkProcessor(); +linkProcessor.SetHandler(_ => true); // suppress Attach frames +``` + +`TestContainerHost` implements `IDisposable` — always declare it with `using var`: + +```csharp +using var host = CreateOpenedContainerHost(endpoint); +``` + +--- + +## Resource Management + +- `TestContainerHost` → `using var` (synchronous `IDisposable`) +- `IConnection` → `await using var` (asynchronous `IAsyncDisposable`) +- `IProducer` / `IConsumer` → `await using var` when you want automatic cleanup; declare without `await using` when you need to `DisposeAsync()` mid-test + +When a test owns several resources whose lifetimes end together, use `DisposeUtil.DisposeAll`: + +```csharp +await DisposeUtil.DisposeAll(producer, connection, host); +``` + +--- + +## Synchronisation + +### Async event waits — use `TaskCompletionSource` + +```csharp +var tcs = new TaskCompletionSource(); +using var cts = new CancellationTokenSource(Timeout); +await using var _ = cts.Token.Register(() => tcs.TrySetCanceled()); +connection.ConnectionClosed += (_, _) => tcs.TrySetResult(true); +// trigger the event... +await tcs.Task; +``` + +### Sync waits inside `TestHandler` — use `ManualResetEvent` or `CountdownEvent` + +```csharp +var attached = new ManualResetEvent(false); +var handler = new TestHandler(@event => +{ + if (@event.Id == EventId.LinkRemoteOpen) + attached.Set(); +}); +// ... +Assert.True(attached.WaitOne(Timeout)); +``` + +### Negative assertions (confirming something does NOT happen) + +For async paths, cancel with `ShortTimeout`: + +```csharp +var cts = new CancellationTokenSource(ShortTimeout); +await Assert.ThrowsAnyAsync( + async () => await consumer.ReceiveAsync(cts.Token)); +``` + +For synchronous paths via `ManualResetEvent`, assert `WaitOne` returns false: + +```csharp +Assert.False(producerAttached.WaitOne(ShortTimeout)); +``` + +--- + +## Arrange-Act-Assert Structure + +Follow a clear, unlabelled AAA structure. Keep each section visually separate with a blank line: + +```csharp +[Fact] +public async Task Should_receive_message() +{ + var endpoint = GetUniqueEndpoint(); + using var host = CreateOpenedContainerHost(endpoint); + var messageSource = host.CreateMessageSource("a1"); + await using var connection = await CreateConnection(endpoint); + var consumer = await connection.CreateConsumerAsync("a1", RoutingType.Anycast); + + messageSource.Enqueue(new Message("foo")); + var message = await consumer.ReceiveAsync(); + + Assert.NotNull(message); + Assert.Equal("foo", message.GetBody()); +} +``` + +The one exception is `Should_create_connection_with_specified_client_id`-style tests where the AAA sections are long and a `// Arrange` / `// Act` / `// Assert` comment genuinely helps navigation — add them only in that case. + +--- + +## Assertions + +Use plain xUnit assertions — no custom extensions, no FluentAssertions: + +```csharp +Assert.Equal("expected", actual); +Assert.NotNull(value); +Assert.Null(value); +Assert.True(condition); +Assert.False(condition); +Assert.Same(expected, actual); +Assert.IsType(instance); +Assert.Contains("substring", message); +Assert.Throws(() => { ... }); +await Assert.ThrowsAsync(() => connection.CreateProducerAsync(...)); +await Assert.ThrowsAnyAsync(async () => await consumer.ReceiveAsync(cts.Token)); +Assert.True(manualResetEvent.WaitOne(Timeout)); +``` + +--- + +## Private Helper Methods + +Extract repeated sequences into private helpers within the same class: + +```csharp +private async Task ShouldSendMessageWithPayload(T payload) +{ + using var host = CreateOpenedContainerHost(); + var messageProcessor = host.CreateMessageProcessor("a1"); + await using var connection = await CreateConnection(host.Endpoint); + await using var producer = await connection.CreateProducerAsync("a1", RoutingType.Anycast); + + await producer.SendAsync(new Message(payload)); + + var received = messageProcessor.Dequeue(Timeout); + Assert.Equal(payload, received.GetBody()); +} + +private async Task<(IProducer, MessageProcessor, TestContainerHost, IConnection)> CreateReattachedProducer() +{ + // ... +} +``` + +Local static functions are also fine for single-test helpers (C# 9+): + +```csharp +static async Task SendMessagesToGroup(TestKit testKit, string address, string groupId, int count) { ... } +``` + +--- + +## Comments + +Add a comment only when the WHY is non-obvious — a hidden constraint, a workaround, or behaviour that would surprise a reader: + +```csharp +// do not send outcome from a remote peer — as a result send should timeout +messageProcessor.SetHandler(_ => true); + +// run on another thread as we don't want to block here +var produceTask = Task.Run(() => producer.Send(new Message("foo"))); +``` + +Never describe what the code does — well-named identifiers already do that. + +--- + +## Summary Checklist + +When writing a new unit test, verify: + +- [ ] File is named `*Spec.cs` and is in the correct project / subdirectory +- [ ] Test class uses the right infrastructure (inherits `ActiveMQNetSpec`, uses `TestKit`, or has no base class) +- [ ] Constructor injects `ITestOutputHelper` when the class inherits `ActiveMQNetSpec` +- [ ] Core and TestKit test methods are `async Task`; pure model methods may be synchronous +- [ ] `TestContainerHost` uses `using var`; connections/producers/consumers use `await using var` +- [ ] Event waits use `TaskCompletionSource` (async) or `ManualResetEvent` / `CountdownEvent` (sync inside `TestHandler`) +- [ ] Negative assertions use `ShortTimeout` (100 ms), not `Timeout` +- [ ] Assertions use plain xUnit — no third-party assertion libraries +- [ ] `[Theory]` used only when the same behaviour needs to be verified across multiple input sets From 04d68a57b56fed63fef2f7efe3a750897a6fbcad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 10:04:42 +0000 Subject: [PATCH 3/3] Address review feedback on test skill docs Agent-Logs-Url: https://github.com/Havret/dotnet-activemq-artemis-client/sessions/4c2aebe1-a91a-44bf-a46e-534b1e37d0da Co-authored-by: Havret <9103861+Havret@users.noreply.github.com> --- .../skills/integration-test-style/SKILL.md | 12 ++++-- .claude/skills/unit-test-style/SKILL.md | 39 +++++++++++++------ 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/.claude/skills/integration-test-style/SKILL.md b/.claude/skills/integration-test-style/SKILL.md index 27535a9..e3c9826 100644 --- a/.claude/skills/integration-test-style/SKILL.md +++ b/.claude/skills/integration-test-style/SKILL.md @@ -14,7 +14,7 @@ This skill describes the required style for all integration tests in this reposi - Files use the `*Spec.cs` suffix (BDD-style), e.g. `MessageAcknowledgementSpec.cs` - Core tests live in `test/ArtemisNetClient.IntegrationTests/` - Extension tests live in their own `*.IntegrationTests/` project -- Subtopics go in subdirectories (e.g. `TopologyManagement/FilterExpressionsSpec.cs`) +- Subtopics go in subdirectories (e.g. `TopologyManagement/CreateAddressSpec.cs`) --- @@ -39,7 +39,7 @@ The base class provides: ## Test Method Conventions - Every test is `async Task` — no synchronous tests -- Use `[Fact]` only — no `[Theory]` +- Use `[Fact]` for most tests; use `[Theory]` with `[InlineData]` or `[MemberData]` when the same behaviour must be verified across a set of inputs - Method names read as sentences: `Should_acknowledge_message`, `Should_send_message_with_priority` ```csharp @@ -48,6 +48,12 @@ public async Task Should_do_something_meaningful() { // ... } + +[Theory, InlineData(RoutingType.Anycast), InlineData(RoutingType.Multicast)] +public async Task Should_behave_the_same_for_all_routing_types(RoutingType routingType) +{ + // ... +} ``` --- @@ -205,7 +211,7 @@ When writing a new integration test, verify: - [ ] File is named `*Spec.cs` - [ ] Class inherits `ActiveMQNetIntegrationSpec` (or uses `TestFixture` for DI tests) - [ ] Constructor injects `ITestOutputHelper` and passes it to `base(output)` -- [ ] All test methods are `async Task` with `[Fact]` +- [ ] All test methods are `async Task` with `[Fact]` or `[Theory]` - [ ] All broker resource names use `Guid.NewGuid().ToString()` - [ ] All disposables use `await using var` - [ ] `CancellationToken` from the base class is passed to every receive call diff --git a/.claude/skills/unit-test-style/SKILL.md b/.claude/skills/unit-test-style/SKILL.md index 032d3d8..8b6f8a6 100644 --- a/.claude/skills/unit-test-style/SKILL.md +++ b/.claude/skills/unit-test-style/SKILL.md @@ -35,7 +35,7 @@ Choose the infrastructure that matches what you're testing: ## Core Unit Tests — Base Class -All core unit test classes inherit from `ActiveMQNetSpec`: +Unit test classes that need in-process AMQP infrastructure inherit from `ActiveMQNetSpec`; pure model tests (no AMQP, no broker) do not need a base class at all: ```csharp public class MyFeatureSpec : ActiveMQNetSpec @@ -66,18 +66,11 @@ The base class provides: ## TestKit Unit Tests — No Base Class -TestKit specs do not inherit from `ActiveMQNetSpec`. Set up AMQP tracing in the constructor and use `EndpointUtil` directly: +TestKit specs do not inherit from `ActiveMQNetSpec`. Use `EndpointUtil` directly to get a unique endpoint, and use `TestKit` directly for the in-process broker: ```csharp public class MyTestKitSpec { - public MyTestKitSpec(ITestOutputHelper output) - { - Trace.TraceLevel = TraceLevel.Information; - var logger = new XUnitLogger(output, "logger"); - Trace.TraceListener += (_, format, args) => logger.LogTrace(format, args); - } - [Fact] public async Task Should_do_something() { @@ -91,6 +84,21 @@ public class MyTestKitSpec } ``` +If you need AMQP frame-level tracing (e.g., to diagnose link-attach sequences), set it up in the constructor — but only when you have an `ITestOutputHelper` and actually need it: + +```csharp +public class MyTestKitSpec +{ + public MyTestKitSpec(ITestOutputHelper output) + { + Trace.TraceLevel = TraceLevel.Information; + var logger = new XUnitLogger(output, "logger"); + Trace.TraceListener += (_, format, args) => logger.LogTrace(format, args); + } + // ... +} +``` + --- ## Pure Model Tests — No Infrastructure @@ -172,17 +180,24 @@ var linkProcessor = host.CreateTestLinkProcessor(); linkProcessor.SetHandler(_ => true); // suppress Attach frames ``` -`TestContainerHost` implements `IDisposable` — always declare it with `using var`: +`TestContainerHost` implements `IDisposable` — prefer `using var` so it is disposed automatically when the test ends, unless you need to dispose it at a specific point mid-test (e.g., to simulate a broker outage): ```csharp +// Preferred — automatic cleanup using var host = CreateOpenedContainerHost(endpoint); + +// When you need to control the disposal point +var host = CreateOpenedContainerHost(endpoint); +// ... use host ... +host.Dispose(); // simulate broker going down +// ... verify recovery behaviour ... ``` --- ## Resource Management -- `TestContainerHost` → `using var` (synchronous `IDisposable`) +- `TestContainerHost` → prefer `using var` (synchronous `IDisposable`); omit `using` only when you need to dispose at a specific point mid-test - `IConnection` → `await using var` (asynchronous `IAsyncDisposable`) - `IProducer` / `IConsumer` → `await using var` when you want automatic cleanup; declare without `await using` when you need to `DisposeAsync()` mid-test @@ -341,7 +356,7 @@ When writing a new unit test, verify: - [ ] Test class uses the right infrastructure (inherits `ActiveMQNetSpec`, uses `TestKit`, or has no base class) - [ ] Constructor injects `ITestOutputHelper` when the class inherits `ActiveMQNetSpec` - [ ] Core and TestKit test methods are `async Task`; pure model methods may be synchronous -- [ ] `TestContainerHost` uses `using var`; connections/producers/consumers use `await using var` +- [ ] `TestContainerHost` uses `using var` (or explicit `Dispose()` only when disposal point matters); connections/producers/consumers use `await using var` - [ ] Event waits use `TaskCompletionSource` (async) or `ManualResetEvent` / `CountdownEvent` (sync inside `TestHandler`) - [ ] Negative assertions use `ShortTimeout` (100 ms), not `Timeout` - [ ] Assertions use plain xUnit — no third-party assertion libraries