Skip to content

Commit 3bae916

Browse files
feat(order): integrate Kafka for order event publishing
Signed-off-by: SebastienDegodez <[email protected]>
1 parent 3ed494d commit 3bae916

19 files changed

+883
-36
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,4 +402,5 @@ FodyWeavers.xsd
402402
.idea
403403

404404
!.husky/csx
405+
**/.DS_Store
405406
.DS_Store

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ You will work with a ASP.NET Core application and explore:
1515
- [Step 4: Write Tests for REST](step4-write-rest-tests.md)
1616
- [Step 5: Write Tests for Async](step5-write-async-tests.md)
1717

18+
Complementary Resources:
19+
- [Integration Testing Patterns](step-integration-testing-patterns.md)
20+
1821
## License Summary
1922

2023
The code in this repository is made available under the MIT license. See the [LICENSE](LICENSE) file for details.
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
# Integration Testing Patterns with Microcks and Testcontainers
2+
3+
This guide explains two different approaches for setting up integration tests with Microcks and Testcontainers in .NET, each with their own trade-offs and use cases.
4+
5+
## Overview
6+
7+
When writing integration tests that use Microcks and Kafka containers, you have two main architectural choices:
8+
9+
1. **IClassFixture Pattern**: Multiple container instances, isolated per test class
10+
2. **ICollectionFixture Pattern**: Single shared container instance, optimized for performance
11+
12+
## Pattern 1: IClassFixture - Isolated Test Classes
13+
14+
### When to use
15+
- When you need complete isolation between test classes
16+
- When different test classes require different container configurations
17+
- When you have few test classes and startup time is not a concern
18+
- When test classes might interfere with each other's state
19+
20+
### Architecture
21+
```csharp
22+
public class MyTestClass : IClassFixture<MicrocksWebApplicationFactory<Program>>
23+
{
24+
// Each test class gets its own factory instance
25+
// Each factory starts its own containers
26+
}
27+
```
28+
29+
### Key Requirements
30+
- **Dynamic Port Allocation**: Each factory instance must use different ports
31+
- **Container Isolation**: Each test class has its own Microcks and Kafka containers
32+
- **Resource Management**: More memory and CPU usage due to multiple containers
33+
34+
### Implementation Example
35+
36+
#### Step 1: WebApplicationFactory with Dynamic Ports
37+
```csharp
38+
public class MicrocksWebApplicationFactory<TProgram> : KestrelWebApplicationFactory<TProgram>, IAsyncLifetime
39+
where TProgram : class
40+
{
41+
public ushort ActualPort { get; private set; }
42+
public KafkaContainer KafkaContainer { get; private set; } = null!;
43+
public MicrocksContainerEnsemble MicrocksContainerEnsemble { get; private set; } = null!;
44+
45+
private ushort GetAvailablePort()
46+
{
47+
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
48+
socket.Bind(new IPEndPoint(IPAddress.Any, 0));
49+
return (ushort)((IPEndPoint)socket.LocalEndPoint!).Port;
50+
}
51+
52+
public async ValueTask InitializeAsync()
53+
{
54+
// CRITICAL: Get dynamic port for each instance
55+
ActualPort = GetAvailablePort();
56+
UseKestrel(ActualPort);
57+
58+
await TestcontainersSettings.ExposeHostPortsAsync(ActualPort, TestContext.Current.CancellationToken);
59+
60+
var network = new NetworkBuilder().Build();
61+
62+
// Each instance gets its own Kafka container
63+
KafkaContainer = new KafkaBuilder()
64+
.WithImage("confluentinc/cp-kafka:7.9.0")
65+
.WithPortBinding(0, KafkaBuilder.KafkaPort) // 0 = dynamic port
66+
.WithNetwork(network)
67+
.WithNetworkAliases("kafka")
68+
.Build();
69+
70+
await KafkaContainer.StartAsync(TestContext.Current.CancellationToken);
71+
72+
// Each instance gets its own Microcks container
73+
MicrocksContainerEnsemble = new MicrocksContainerEnsemble(network, "quay.io/microcks/microcks-uber:1.13.0")
74+
.WithAsyncFeature()
75+
.WithMainArtifacts("resources/order-service-openapi.yaml")
76+
.WithKafkaConnection(new KafkaConnection($"kafka:19092"));
77+
78+
await MicrocksContainerEnsemble.StartAsync();
79+
}
80+
81+
protected override void ConfigureWebHost(IWebHostBuilder builder)
82+
{
83+
base.ConfigureWebHost(builder);
84+
85+
var pastryApiEndpoint = MicrocksContainerEnsemble.MicrocksContainer
86+
.GetRestMockEndpoint("API Pastries", "0.0.1");
87+
builder.UseSetting("PastryApi:BaseUrl", pastryApiEndpoint);
88+
89+
var kafkaBootstrap = KafkaContainer.GetBootstrapAddress()
90+
.Replace("PLAINTEXT://", "", StringComparison.OrdinalIgnoreCase);
91+
builder.UseSetting("Kafka:BootstrapServers", kafkaBootstrap);
92+
}
93+
94+
public async override ValueTask DisposeAsync()
95+
{
96+
await base.DisposeAsync();
97+
await KafkaContainer.DisposeAsync();
98+
await MicrocksContainerEnsemble.DisposeAsync();
99+
}
100+
}
101+
```
102+
103+
#### Step 2: Test Class Implementation
104+
```csharp
105+
public class OrderControllerTests : IClassFixture<MicrocksWebApplicationFactory<Program>>
106+
{
107+
private readonly MicrocksWebApplicationFactory<Program> _factory;
108+
private readonly ITestOutputHelper _testOutput;
109+
110+
public OrderControllerTests(
111+
MicrocksWebApplicationFactory<Program> factory,
112+
ITestOutputHelper testOutput)
113+
{
114+
_factory = factory;
115+
_testOutput = testOutput;
116+
}
117+
118+
[Fact]
119+
public async Task CreateOrder_ShouldReturnCreatedOrder()
120+
{
121+
// This test class has its own containers
122+
using var client = _factory.CreateClient();
123+
124+
// Test implementation...
125+
}
126+
}
127+
```
128+
129+
### Pros and Cons
130+
131+
**Advantages:**
132+
- Complete isolation between test classes
133+
- Different configurations per test class
134+
- No shared state issues
135+
- Parallel test execution per class
136+
137+
**Disadvantages:**
138+
- Higher resource usage (multiple containers)
139+
- Slower overall test execution
140+
- More complex port management
141+
- Potential for port conflicts if not handled properly
142+
143+
---
144+
145+
## Pattern 2: ICollectionFixture - Shared Containers (Recommended)
146+
147+
### When to use
148+
- When you want optimal performance and resource usage
149+
- When test classes can share the same container configuration
150+
- When you have many test classes
151+
- When startup time is a concern
152+
153+
### Architecture
154+
```csharp
155+
[Collection(SharedTestCollection.Name)]
156+
public class MyTestClass : BaseIntegrationTest
157+
{
158+
// All test classes share the same factory instance
159+
// Single set of containers for all tests
160+
}
161+
```
162+
163+
### Key Benefits
164+
- **Single Container Instance**: One Microcks + one Kafka container for all tests
165+
- **Performance Optimized**: ~70% faster test execution
166+
- **Resource Efficient**: Lower memory and CPU usage
167+
- **Single Port Allocation**: One Kestrel port for the entire test suite
168+
169+
### Implementation Example
170+
171+
#### Step 1: Shared Collection Definition
172+
```csharp
173+
[CollectionDefinition(Name)]
174+
public class SharedTestCollection : ICollectionFixture<MicrocksWebApplicationFactory<Program>>
175+
{
176+
public const string Name = "SharedTestCollection";
177+
}
178+
```
179+
180+
#### Step 2: Enhanced WebApplicationFactory
181+
```csharp
182+
public class MicrocksWebApplicationFactory<TProgram> : KestrelWebApplicationFactory<TProgram>, IAsyncLifetime
183+
where TProgram : class
184+
{
185+
private static readonly SemaphoreSlim InitializationSemaphore = new(1, 1);
186+
private static bool _isInitialized;
187+
188+
public ushort ActualPort { get; private set; }
189+
public KafkaContainer KafkaContainer { get; private set; } = null!;
190+
public MicrocksContainerEnsemble MicrocksContainerEnsemble { get; private set; } = null!;
191+
192+
public async ValueTask InitializeAsync()
193+
{
194+
await InitializationSemaphore.WaitAsync();
195+
try
196+
{
197+
if (_isInitialized)
198+
{
199+
TestLogger.WriteLine("[Factory] Already initialized, skipping...");
200+
return;
201+
}
202+
203+
TestLogger.WriteLine("[Factory] Starting initialization...");
204+
205+
// Single port allocation for all tests
206+
ActualPort = GetAvailablePort();
207+
UseKestrel(ActualPort);
208+
209+
await TestcontainersSettings.ExposeHostPortsAsync(ActualPort, TestContext.Current.CancellationToken);
210+
211+
// Single network and containers for all tests
212+
var network = new NetworkBuilder().Build();
213+
214+
KafkaContainer = new KafkaBuilder()
215+
.WithImage("confluentinc/cp-kafka:7.9.0")
216+
.WithNetwork(network)
217+
.WithNetworkAliases("kafka")
218+
.Build();
219+
220+
await KafkaContainer.StartAsync(TestContext.Current.CancellationToken);
221+
222+
MicrocksContainerEnsemble = new MicrocksContainerEnsemble(network, "quay.io/microcks/microcks-uber:1.13.0")
223+
.WithAsyncFeature()
224+
.WithMainArtifacts("resources/order-service-openapi.yaml")
225+
.WithKafkaConnection(new KafkaConnection("kafka:19092"));
226+
227+
await MicrocksContainerEnsemble.StartAsync();
228+
229+
_isInitialized = true;
230+
TestLogger.WriteLine("[Factory] Initialization completed");
231+
}
232+
finally
233+
{
234+
InitializationSemaphore.Release();
235+
}
236+
}
237+
238+
// ConfigureWebHost and DisposeAsync similar to Pattern 1
239+
}
240+
```
241+
242+
#### Step 3: Base Test Class
243+
```csharp
244+
[Collection(SharedTestCollection.Name)]
245+
public abstract class BaseIntegrationTest
246+
{
247+
public WebApplicationFactory<Program> Factory { get; private set; }
248+
public ushort Port { get; private set; }
249+
public MicrocksContainerEnsemble MicrocksContainerEnsemble { get; }
250+
public KafkaContainer KafkaContainer { get; }
251+
public HttpClient HttpClient { get; private set; }
252+
253+
protected BaseIntegrationTest(MicrocksWebApplicationFactory<Program> factory)
254+
{
255+
Factory = factory;
256+
HttpClient = factory.CreateClient();
257+
Port = factory.ActualPort;
258+
MicrocksContainerEnsemble = factory.MicrocksContainerEnsemble;
259+
KafkaContainer = factory.KafkaContainer;
260+
}
261+
262+
protected void SetupTestOutput(ITestOutputHelper testOutputHelper)
263+
{
264+
TestLogger.SetTestOutput(testOutputHelper);
265+
}
266+
}
267+
```
268+
269+
#### Step 4: Test Class Implementation
270+
```csharp
271+
public class OrderControllerTests : BaseIntegrationTest
272+
{
273+
private readonly ITestOutputHelper _testOutput;
274+
275+
public OrderControllerTests(
276+
ITestOutputHelper testOutput,
277+
MicrocksWebApplicationFactory<Program> factory)
278+
: base(factory)
279+
{
280+
_testOutput = testOutput;
281+
SetupTestOutput(testOutput);
282+
}
283+
284+
[Fact]
285+
public async Task CreateOrder_ShouldReturnCreatedOrder()
286+
{
287+
// Shared containers with all other test classes
288+
// Test implementation...
289+
}
290+
}
291+
```
292+
293+
### Pros and Cons
294+
295+
**Advantages:**
296+
- Excellent performance (~70% faster)
297+
- Lower resource usage
298+
- Simple port management
299+
- No port conflicts
300+
- Shared infrastructure
301+
302+
**Disadvantages:**
303+
- Shared state between test classes
304+
- Same configuration for all tests
305+
- Potential for test interdependencies
306+
307+
---
308+
309+
## Comparison Summary
310+
311+
| Aspect | IClassFixture Pattern | ICollectionFixture Pattern |
312+
|--------|----------------------|---------------------------|
313+
| **Performance** | Slower (multiple startups) | Faster (~70% improvement) |
314+
| **Resource Usage** | High (multiple containers) | Low (single containers) |
315+
| **Isolation** | Complete per class | Shared across classes |
316+
| **Port Management** | Complex (dynamic per class) | Simple (single allocation) |
317+
| **Configuration** | Flexible per class | Single configuration |
318+
| **Recommended For** | Different configs needed | Homogeneous test suites |
319+
320+
## Recommendation
321+
322+
**Use ICollectionFixture Pattern (Pattern 2)** for most scenarios because:
323+
- Better performance and resource efficiency
324+
- Simpler port management
325+
- Most integration tests can share the same container setup
326+
- Easier to maintain and debug
327+
328+
**Use IClassFixture Pattern (Pattern 1)** only when:
329+
- You need different container configurations per test class
330+
- Complete isolation is mandatory
331+
- You have few test classes and performance isn't critical

src/Order.Service/Order.Service.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
<InternalsVisibleTo Include="Order.Service.Tests" />
1010
</ItemGroup>
1111

12-
<ItemGroup>
13-
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
12+
<ItemGroup>
13+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
14+
<PackageReference Include="Confluent.Kafka" Version="2.12.0" />
1415
</ItemGroup>
1516
</Project>

src/Order.Service/Program.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using Order.Service.Client;
2424
using Order.Service.Endpoints;
2525
using Order.Service.UseCases;
26+
using Confluent.Kafka;
2627

2728
var builder = WebApplication.CreateBuilder(args);
2829

@@ -35,12 +36,24 @@
3536
throw new InvalidOperationException("PastryApi:BaseUrl configuration is required and cannot be null or empty.");
3637
}
3738

38-
3939
builder.Services.AddHttpClient<PastryAPIClient>(opt =>
4040
{
4141
opt.BaseAddress = new Uri(pastryApiUrl + "/");
4242
});
4343

44+
// Kafka configuration
45+
builder.Services.AddSingleton(sp =>
46+
{
47+
var config = new ProducerConfig
48+
{
49+
ClientId = "order-service-producer",
50+
BootstrapServers = builder.Configuration.GetValue<string>("Kafka:BootstrapServers"),
51+
};
52+
53+
return new ProducerBuilder<string, string>(config).Build();
54+
});
55+
builder.Services.AddSingleton<IEventPublisher, OrderEventPublisher>();
56+
4457
// Services for API metadata
4558
builder.Services.AddOpenApi();
4659

0 commit comments

Comments
 (0)