implementation reference
Write Integration Tests
Generates comprehensive TestHost-based integration tests for MagicOnion services, accessors, managers, and engines. Handles Docker container setup, service registration, and assertion patterns.
Write Integration Tests Skill
Persona
⚔️ Quinn the War Marshal
"HEAR ME, KNIGHTS! Before we storm the castle, we must test our siege engines!"
Quinn orchestrates the testing battlefield, ensuring every service is battle-tested with real infrastructure before deployment.
Goal
Generate production-quality integration tests using the Basis.Testing TestHost pattern with Docker containers for real dependencies (PostgreSQL, RabbitMQ, Azurite).
When to Use
- Creating new integration test classes for services
- Adding test coverage for new MagicOnion operations
- Testing MassTransit sagas and message handlers
- Validating Marten document storage operations
- Testing end-to-end request/response flows
Prerequisites
- Service implementation exists (interface + implementation)
- Understanding of what the service does (from spec.md or design.md)
- Container dependencies identified (database, message bus, etc.)
Inputs
Required:
| Input | Description |
|---|---|
| Service Interface | The MagicOnion service interface to test (e.g., IAdminManager) |
| Service Implementation | The implementation class (e.g., AdminManager) |
| Test Scenarios | What operations/flows to test |
Optional:
| Input | Description |
|---|---|
| spec.md | Spec file with acceptance criteria |
| design.md | Design document with edge cases |
| Existing tests | Reference tests in the same project |
Outputs
| File | Description |
|---|---|
*TestHostFixture.cs | Fixture class implementing IAsyncLifetime for TestHost setup |
*Tests.cs | Test class with [Fact] methods using the fixture |
Reference Files
- Fixture Example: PayrollOnboardingSagaTestHostFixture.cs
- Test Example: PayrollOnboardingSagaTests.cs
- PostgresContainer: PostgresContainer.cs
- RabbitMqContainer: MassTransitRabbitMqContainer.cs
- AzuriteContainer: AzuriteContainer.cs
- TestHost API: TestHost.cs
- Instructions: integration.tests.instructions.md
Workflow
Step 1: Analyze Service Under Test
-
Identify the service interface and implementation:
Interface: IAdminManager (in *.Interfaces project) Implementation: AdminManager (in *.Service project) -
Identify dependencies:
- Database (Marten/PostgreSQL)?
- Message bus (MassTransit/RabbitMQ)?
- Blob storage (Azurite)?
- Other services (fake or real)?
-
List operations to test:
- Happy path scenarios
- Validation error scenarios
- Edge cases from spec.md
Step 2: Create Test Project Structure
Ensure the test project exists at src/services/{layer}/{domain}/Lista.{Layer}.{Domain}.Tests/
Required project references:
<ItemGroup>
<PackageReference Include="Basis.Testing" />
<PackageReference Include="FakeItEasy" />
<PackageReference Include="Shouldly" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\testing\Lista.Common.Testing\Lista.Common.Testing.csproj" />
<ProjectReference Include="..\Lista.{Layer}.{Domain}.Service\Lista.{Layer}.{Domain}.Service.csproj" />
</ItemGroup>
Step 3: Generate Fixture Class
Create {Feature}TestHostFixture.cs:
using Basis.Core.Mediation;
using Basis.Testing;
using Lista.Common.Testing;
using Marten;
using MassTransit;
using Microsoft.Extensions.DependencyInjection;
namespace Lista.{Layer}.{Domain}.Tests;
public sealed class {Feature}TestHostFixture : IAsyncLifetime
{
private TestHost? _testHost;
public TestHost TestHost => _testHost
?? throw new InvalidOperationException("Fixture not initialized");
public async Task InitializeAsync()
{
_testHost = await CreateTestHost();
}
public async Task DisposeAsync()
{
if (_testHost != null)
{
await _testHost.DisposeAsync();
}
}
private static async Task<TestHost> CreateTestHost()
{
var testHost = await new TestHost()
.WithDockerContainer<PostgresContainer>()
// Add .WithDockerContainer<MassTransitRabbitMqContainer>() if using MassTransit
.ConfigureServices((services, containers) =>
{
services.AddGrpc(options => options.EnableDetailedErrors = true);
var postgres = containers.Get<PostgresContainer>();
services.AddMarten(options =>
{
options.Connection(postgres.ConnectionString);
options.AutoCreateSchemaObjects = AutoCreate.All;
});
// Register mediator and handlers
services.AddScoped<IMediator, Mediator>();
// services.AddScoped<IRequestHandler<TRequest, TResponse>, THandler>();
})
.WithService<I{ServiceInterface}, {ServiceImplementation}>()
.StartAsync();
return testHost;
}
}
Step 4: Generate Test Class
Create {Feature}Tests.cs:
using Basis.Testing;
using Shouldly;
using Xunit.Abstractions;
namespace Lista.{Layer}.{Domain}.Tests;
public class {Feature}Tests : IClassFixture<{Feature}TestHostFixture>
{
private readonly {Feature}TestHostFixture _fixture;
private readonly ITestOutputHelper _testOutputHelper;
public {Feature}Tests({Feature}TestHostFixture fixture, ITestOutputHelper testOutputHelper)
{
_fixture = fixture;
_testOutputHelper = testOutputHelper;
}
#region Happy Path Tests
[Fact]
public async Task {Operation}_Returns_ExpectedResult()
{
// Arrange
var client = _fixture.TestHost.CreateMagicOnionClient<I{Service}>();
var request = new {RequestType}
{
// Set properties
};
// Act
var response = await client.{Method}(request) as {SuccessResponseType};
// Assert
response.ShouldNotBeNull();
// Add specific assertions
}
#endregion
#region Validation Tests
[Fact]
public async Task {Operation}_Returns_ValidationError_For_InvalidInput()
{
// Arrange
var client = _fixture.TestHost.CreateMagicOnionClient<I{Service}>();
var request = new {RequestType}
{
// Set invalid properties
};
// Act
var response = await client.{Method}(request) as {Layer}Error;
// Assert
response.ShouldNotBeNull();
response.Code.ShouldBe({Layer}ErrorCode.ValidationFailed);
response.Message.ShouldContain("{FieldName}");
}
#endregion
}
Step 5: Add Container-Specific Configuration
For MassTransit/Sagas:
.WithDockerContainer<MassTransitRabbitMqContainer>()
.ConfigureServices((services, containers) =>
{
var rabbit = containers.Get<MassTransitRabbitMqContainer>();
services.AddMassTransit(x =>
{
x.AddSagaStateMachine<MyStateMachine, MySagaState>()
.InMemoryRepository();
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(rabbit.Host, (ushort)rabbit.AmqpPort, "/", h =>
{
h.Username(rabbit.Username);
h.Password(rabbit.Password);
});
cfg.ConfigureEndpoints(context);
});
});
})
For Faked Dependencies:
var fakeMagicOnionClientFactory = A.Fake<IMagicOnionClientFactory>();
var fakeService = A.Fake<IOtherService>();
A.CallTo(() => fakeMagicOnionClientFactory.CreateClient<IOtherService>())
.Returns(fakeService);
A.CallTo(() => fakeService.SomeMethod(A<SomeRequest>._))
.ReturnsLazily(call =>
{
var request = call.GetArgument<SomeRequest>(0);
return new UnaryResult<SomeResponse>(new SomeResponse { Success = true });
});
services.AddSingleton(fakeMagicOnionClientFactory);
Test Patterns
Delay Constants for Async Operations
/// <summary>Short delay for quick state transitions.</summary>
private const int ShortDelayMs = 500;
/// <summary>Standard delay for saga state transitions.</summary>
private const int StandardDelayMs = 1000;
/// <summary>Extended delay for multiple state transitions.</summary>
private const int ExtendedDelayMs = 2000;
/// <summary>Long delay for timeout scenarios.</summary>
private const int TimeoutDelayMs = 5000;
Common Test Categories
- Happy Path Tests - Normal successful operations
- Validation Tests - Input validation errors
- Edge Case Tests - Boundary conditions, null handling
- Normalization Tests - Input trimming, case handling
- Full Integration Tests - End-to-end with real dependencies
Response Type Assertions
// Success response
var response = await client.Method(request) as SuccessResponse;
response.ShouldNotBeNull();
response.PropertyName.ShouldBe(expectedValue);
// Error response
var response = await client.Method(request) as ManagerError;
response.ShouldNotBeNull();
response.Code.ShouldBe(ManagerErrorCode.ValidationFailed);
response.Message.ShouldContain("FieldName");
Checklist
Before completing:
- Fixture implements
IAsyncLifetime - All required Docker containers are provisioned
- Services configured with container connection strings
-
EnableDetailedErrors = trueset for gRPC - Marten uses
AutoCreate.All(tests only) - Tests use
CreateMagicOnionClient<TService>()(NOTCreateGrpcClient) - Both success and failure scenarios covered
- Test data uses realistic domain values
- Resources disposed in
DisposeAsync - Assertions validate spec-compliant responses
- Async operations have appropriate delays
Anti-Patterns to Avoid
| ❌ Don't | ✅ Do |
|---|---|
| Hardcode container ports | Let containers auto-assign ports |
Use CreateGrpcClient<T>() | Use CreateMagicOnionClient<TService>() |
| Share static state between tests | Keep tests isolated |
| Skip container disposal | Always await DisposeAsync() |
| Use placeholder test data | Use realistic domain values |
Copy AutoCreate.All to production | Keep it in tests only |
| Reuse containers across unrelated tests | Use fixture per test class |
Troubleshooting
| Problem | Solution |
|---|---|
| Container health check fails | Increase timeout or check connection strings |
| Marten failures | Verify database name/credentials match container |
| gRPC unhandled exceptions | Enable EnableDetailedErrors in test config |
| Flaky tests | Don't reuse containers across tests |
| Port conflicts | Don't hardcode ports, use auto-assignment |
Example Invocation
User: "Write integration tests for the ProfileAccess service that tests Store and Fetch operations"
Agent response:
- Analyzes
IProfileAccessinterface - Identifies PostgreSQL dependency (Marten)
- Creates
ProfileAccessTestHostFixture.cs - Creates
ProfileAccessTests.cswith:- Store happy path test
- Store validation error tests
- Fetch happy path test
- Fetch not found test
- Runs tests to verify they pass
Related Skills
magic-accessor-implementation- For implementing the service being testedintegration-test-harness- General guidance on TestHost patternsmarten-document-modeling- For understanding document storage patterns