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.cssuffix (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/CreateAddressSpec.cs)
Base Class
All core integration test classes inherit from ActiveMQNetIntegrationSpec:
public class MyFeatureSpec : ActiveMQNetIntegrationSpec
{
public MyFeatureSpec(ITestOutputHelper output) : base(output) { }
}
The base class provides:
CreateConnection()— returns anIConnectionbacked by aConnectionFactorywired toXUnitLoggerFactorywith GUID message IDsCancellationToken— 1 minute in DEBUG, 10 seconds in ReleaseGetEndpoint()— 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]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
[Fact]
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)
{
// ...
}
Arrange-Act-Assert Structure
Tests follow a clear, unlabelled AAA structure. Keep each section visually separate with a blank line.
[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<OperationCanceledException>(
async () => await consumer2.ReceiveAsync(
new CancellationTokenSource(TimeSpan.FromMilliseconds(500)).Token));
}
Resource Management
- Declare all disposables with
await using varso 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 withawait usingin 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.
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:
await Assert.ThrowsAsync<OperationCanceledException>(
async () => await consumer.ReceiveAsync(
new CancellationTokenSource(TimeSpan.FromMilliseconds(500)).Token));
Assertions
Use plain xUnit assertions — no custom extensions, no FluentAssertions:
Assert.Equal("expected", actual);
Assert.NotNull(value);
Assert.Single(collection);
Assert.All(collection, item => Assert.Equal("x", item));
await Assert.ThrowsAsync<InvalidOperationException>(...);
await Assert.ThrowsAnyAsync<Exception>(...);
Private Helper Methods
Extract repeated sequences into private static async Task helpers within the same class:
private static async Task<IReadOnlyList<Message>> ReceiveMessages(
IConsumer consumer, int count)
{
var messages = new List<Message>();
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:
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<TestProducer>(address, RoutingType.Anycast));
var producer = fixture.Services.GetRequiredService<TestProducer>();
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:
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
*.IntegrationTestsproject - File is named
*Spec.cs - Class inherits
ActiveMQNetIntegrationSpec(or usesTestFixturefor DI tests) - Constructor injects
ITestOutputHelperand passes it tobase(output) - All test methods are
async Taskwith[Fact]or[Theory] - All broker resource names use
Guid.NewGuid().ToString() - All disposables use
await using var -
CancellationTokenfrom the base class is passed to every receive call - Negative assertions use a short
CancellationTokenSourcetimeout - Assertions use plain xUnit — no third-party assertion libraries
Source: Havret/dotnet-activemq-artemis-client — distributed by TomeVault.