Skip to content

Commit dcb2e7f

Browse files
fix: PR review
Signed-off-by: SebastienDegodez <sebastien.degodez@gmail.com>
1 parent c7b064c commit dcb2e7f

11 files changed

+66
-64
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ You will work with a ASP.NET Core application and explore:
1616
- [Step 5: Write Tests for Async](step5-write-async-tests.md)
1717

1818
Complementary Resources:
19-
- [Integration Testing Patterns](step-integration-testing-patterns.md)
19+
- [Integration Testing Patterns](explain-integration-testing-patterns.md)
2020

2121
## License Summary
2222

explain-integration-testing-patterns.md

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This guide explains two different approaches for setting up integration tests wi
77
When writing integration tests that use Microcks and Kafka containers, you have two main architectural choices:
88

99
1. **IClassFixture Pattern**: Multiple container instances, isolated per test class
10-
2. **ICollectionFixture Pattern**: Single shared container instance, optimized for performance
10+
2. **ICollectionFixture Pattern**: A set of shared container instances (e.g., one Microcks container and one Kafka container) used by all test classes for optimal performance and resource efficiency
1111

1212
## Pattern 1: IClassFixture - Isolated Test Classes
1313

@@ -19,7 +19,7 @@ When writing integration tests that use Microcks and Kafka containers, you have
1919

2020
### Architecture
2121
```csharp
22-
public class MyTestClass : IClassFixture<MicrocksWebApplicationFactory<Program>>
22+
public class MyTestClass : IClassFixture<OrderServiceWebApplicationFactory<Program>>
2323
{
2424
// Each test class gets its own factory instance
2525
// Each factory starts its own containers
@@ -34,8 +34,15 @@ public class MyTestClass : IClassFixture<MicrocksWebApplicationFactory<Program>>
3434
### Implementation Example
3535

3636
#### Step 1: WebApplicationFactory with Dynamic Ports
37+
38+
> **Note:**
39+
> The implementation of `OrderServiceWebApplicationFactory` (or `MicrocksWebApplicationFactory`) must be adapted depending on the fixture pattern:
40+
>
41+
> - With **IClassFixture**, each test class gets its own factory and containers. You must allocate **dynamic ports** for Kestrel, Kafka, and all services, because static port mapping (like `kafka:9092:9092`) is not possible—otherwise, you will have port conflicts if tests run in parallel. All port assignments must be programmatic and injected into your test server and mocks.
42+
>
43+
> - With **ICollectionFixture** (shared collection), a single factory and set of containers are shared for all tests. You allocate ports only once, which simplifies configuration and avoids conflicts. This is why the shared collection pattern is recommended for most test suites.
3744
```csharp
38-
public class MicrocksWebApplicationFactory<TProgram> : KestrelWebApplicationFactory<TProgram>, IAsyncLifetime
45+
public class OrderServiceWebApplicationFactory<TProgram> : KestrelWebApplicationFactory<TProgram>, IAsyncLifetime
3946
where TProgram : class
4047
{
4148
public ushort ActualPort { get; private set; }
@@ -102,13 +109,13 @@ public class MicrocksWebApplicationFactory<TProgram> : KestrelWebApplicationFact
102109

103110
#### Step 2: Test Class Implementation
104111
```csharp
105-
public class OrderControllerTests : IClassFixture<MicrocksWebApplicationFactory<Program>>
112+
public class OrderControllerTests : IClassFixture<OrderServiceWebApplicationFactory<Program>>
106113
{
107-
private readonly MicrocksWebApplicationFactory<Program> _factory;
114+
private readonly OrderServiceWebApplicationFactory<Program> _factory;
108115
private readonly ITestOutputHelper _testOutput;
109116

110117
public OrderControllerTests(
111-
MicrocksWebApplicationFactory<Program> factory,
118+
OrderServiceWebApplicationFactory<Program> factory,
112119
ITestOutputHelper testOutput)
113120
{
114121
_factory = factory;
@@ -153,15 +160,15 @@ public class OrderControllerTests : IClassFixture<MicrocksWebApplicationFactory<
153160
### Architecture
154161
```csharp
155162
[Collection(SharedTestCollection.Name)]
156-
public class MyTestClass : BaseIntegrationTest
163+
public class MyTestClass
157164
{
158165
// All test classes share the same factory instance
159166
// Single set of containers for all tests
160167
}
161168
```
162169

163170
### Key Benefits
164-
- **Single Container Instance**: One Microcks + one Kafka container for all tests
171+
- **Single set of containers Instances**: One Microcks + one Kafka container for all tests
165172
- **Performance Optimized**: ~70% faster test execution
166173
- **Resource Efficient**: Lower memory and CPU usage
167174
- **Single Port Allocation**: One Kestrel port for the entire test suite
@@ -171,15 +178,15 @@ public class MyTestClass : BaseIntegrationTest
171178
#### Step 1: Shared Collection Definition
172179
```csharp
173180
[CollectionDefinition(Name)]
174-
public class SharedTestCollection : ICollectionFixture<MicrocksWebApplicationFactory<Program>>
181+
public class SharedTestCollection : ICollectionFixture<OrderServiceWebApplicationFactory<Program>>
175182
{
176183
public const string Name = "SharedTestCollection";
177184
}
178185
```
179186

180187
#### Step 2: Enhanced WebApplicationFactory
181188
```csharp
182-
public class MicrocksWebApplicationFactory<TProgram> : KestrelWebApplicationFactory<TProgram>, IAsyncLifetime
189+
public class OrderServiceWebApplicationFactory<TProgram> : KestrelWebApplicationFactory<TProgram>, IAsyncLifetime
183190
where TProgram : class
184191
{
185192
private static readonly SemaphoreSlim InitializationSemaphore = new(1, 1);
@@ -250,7 +257,7 @@ public abstract class BaseIntegrationTest
250257
public KafkaContainer KafkaContainer { get; }
251258
public HttpClient HttpClient { get; private set; }
252259

253-
protected BaseIntegrationTest(MicrocksWebApplicationFactory<Program> factory)
260+
protected BaseIntegrationTest(OrderServiceWebApplicationFactory<Program> factory)
254261
{
255262
Factory = factory;
256263
HttpClient = factory.CreateClient();
@@ -274,7 +281,7 @@ public class OrderControllerTests : BaseIntegrationTest
274281

275282
public OrderControllerTests(
276283
ITestOutputHelper testOutput,
277-
MicrocksWebApplicationFactory<Program> factory)
284+
OrderServiceWebApplicationFactory<Program> factory)
278285
: base(factory)
279286
{
280287
_testOutput = testOutput;

tests/Order.Service.Tests/Api/OrderControllerContractTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class OrderControllerContractTests : BaseIntegrationTest
3434

3535
public OrderControllerContractTests(
3636
ITestOutputHelper testOutputHelper,
37-
MicrocksWebApplicationFactory<Program> factory)
37+
OrderServiceWebApplicationFactory<Program> factory)
3838
: base(factory)
3939
{
4040
TestOutputHelper = testOutputHelper;

tests/Order.Service.Tests/Api/OrderControllerPostmanContractTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class OrderControllerPostmanContractTests : BaseIntegrationTest
3030

3131
public OrderControllerPostmanContractTests(
3232
ITestOutputHelper testOutputHelper,
33-
MicrocksWebApplicationFactory<Program> factory)
33+
OrderServiceWebApplicationFactory<Program> factory)
3434
: base(factory)
3535
{
3636
TestOutputHelper = testOutputHelper;

tests/Order.Service.Tests/BaseIntegrationTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public abstract class BaseIntegrationTest
4141
public KafkaContainer KafkaContainer { get; }
4242
public HttpClient? HttpClient { get; private set; }
4343

44-
protected BaseIntegrationTest(MicrocksWebApplicationFactory<Program> factory)
44+
protected BaseIntegrationTest(OrderServiceWebApplicationFactory<Program> factory)
4545
{
4646
Factory = factory;
4747

tests/Order.Service.Tests/Client/PastryAPIClientTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public class PastryAPIClientTests : BaseIntegrationTest
3636

3737
public PastryAPIClientTests(
3838
ITestOutputHelper testOutputHelper,
39-
MicrocksWebApplicationFactory<Program> factory)
39+
OrderServiceWebApplicationFactory<Program> factory)
4040
: base(factory)
4141
{
4242
TestOutputHelper = testOutputHelper;

tests/Order.Service.Tests/Order.Service.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9+
<PackageReference Include="Awaitility.NET" Version="1.0.2" />
910
<PackageReference Include="coverlet.collector" Version="6.0.4">
1011
<PrivateAssets>all</PrivateAssets>
1112
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

tests/Order.Service.Tests/MicrocksWebApplicationFactory.cs renamed to tests/Order.Service.Tests/OrderServiceWebApplicationFactory.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ namespace Order.Service.Tests;
4242
/// This factory is designed to be used as a singleton across all test classes to optimize container startup time.
4343
/// Containers are started once and reused by all tests in the test assembly.
4444
/// </summary>
45-
public class MicrocksWebApplicationFactory<TProgram> : KestrelWebApplicationFactory<TProgram>, IAsyncLifetime
45+
public class OrderServiceWebApplicationFactory<TProgram> : KestrelWebApplicationFactory<TProgram>, IAsyncLifetime
4646
where TProgram : class
4747
{
48-
private const string MicrocksImage = "quay.io/microcks/microcks-uber:1.13.0";
48+
private const string MicrocksImage = "quay.io/microcks/microcks-uber:1.13.0-native";
4949

5050
private static readonly SemaphoreSlim InitializationSemaphore = new(1, 1);
5151
private static bool _isInitialized;

tests/Order.Service.Tests/SharedTestCollection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ namespace Order.Service.Tests;
2424
/// This guarantees that containers are started only once across all test classes.
2525
/// </summary>
2626
[CollectionDefinition(Name)]
27-
public class SharedTestCollection : ICollectionFixture<MicrocksWebApplicationFactory<Program>>
27+
public class SharedTestCollection : ICollectionFixture<OrderServiceWebApplicationFactory<Program>>
2828
{
2929
public const string Name = "SharedTestCollection";
3030
}

tests/Order.Service.Tests/UseCases/OrderEventListenerTests.cs

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
using Order.Service.UseCases;
2424
using Order.Service.UseCases.Model;
2525
using OrderModel = Order.Service.UseCases.Model.Order;
26-
26+
using static Awaitility.Awaitility;
2727
using Xunit;
2828

2929
namespace Order.Service.Tests.UseCases;
@@ -34,7 +34,7 @@ public class OrderEventListenerTests : BaseIntegrationTest
3434

3535
public OrderEventListenerTests(
3636
ITestOutputHelper testOutputHelper,
37-
MicrocksWebApplicationFactory<Program> factory)
37+
OrderServiceWebApplicationFactory<Program> factory)
3838
: base(factory)
3939
{
4040
TestOutputHelper = testOutputHelper;
@@ -52,11 +52,41 @@ public async Task TestEventIsConsumedAndProcessedByService()
5252
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
5353
var consumerTask = StartOrderEventConsumerAsync(cts.Token);
5454

55+
var orderUseCase = Factory.Services.GetRequiredService<OrderUseCase>();
5556
OrderModel? order = null;
56-
// Act & Assert - Polling pattern similar to the Java example
57+
58+
// Act & Assert
5759
try
5860
{
59-
order = await PollForOrderProcessingAsync(expectedOrderId, cts);
61+
Await().
62+
AtMost(TimeSpan.FromSeconds(4))
63+
.PollDelay(TimeSpan.FromMilliseconds(400))
64+
.PollInterval(TimeSpan.FromMilliseconds(400))
65+
.Until(() =>
66+
{
67+
try
68+
{
69+
var retrievedOrder = orderUseCase.GetOrderAsync(expectedOrderId, TestContext.Current.CancellationToken).Result;
70+
if (retrievedOrder != null)
71+
{
72+
TestOutputHelper.WriteLine($"Order {retrievedOrder.Id} successfully processed!");
73+
order = retrievedOrder;
74+
cts.Cancel(); // Cancel the consumer after successful processing
75+
return true;
76+
}
77+
return false;
78+
}
79+
catch (OrderNotFoundException)
80+
{
81+
TestOutputHelper.WriteLine($"Order {expectedOrderId} not found yet, continuing to poll...");
82+
return false;
83+
}
84+
catch (AggregateException ex) when (ex.InnerException is OrderNotFoundException)
85+
{
86+
TestOutputHelper.WriteLine($"Order {expectedOrderId} not found yet, continuing to poll...");
87+
return false;
88+
}
89+
});
6090

6191
Assert.NotNull(order);
6292
// Verify the order properties match expected values
@@ -84,43 +114,7 @@ public async Task TestEventIsConsumedAndProcessedByService()
84114
}
85115
}
86116

87-
private async Task<OrderModel> PollForOrderProcessingAsync(
88-
string expectedOrderId,
89-
CancellationTokenSource cts)
90-
{
91-
var pollDelay = TimeSpan.FromMilliseconds(400);
92-
var pollInterval = TimeSpan.FromMilliseconds(400);
93-
var startTime = DateTime.UtcNow;
94-
95-
var orderUseCase = Factory.Services.GetRequiredService<OrderUseCase>();
96-
// Initial delay
97-
await Task.Delay(pollDelay, TestContext.Current.CancellationToken);
98117

99-
while (DateTime.UtcNow - startTime < TimeSpan.FromSeconds(4))
100-
{
101-
try
102-
{
103-
// Simulate getting order by ID (this would normally call a GET endpoint)
104-
var order = await orderUseCase.GetOrderAsync(expectedOrderId, TestContext.Current.CancellationToken);
105-
106-
if (order is not null)
107-
{
108-
TestOutputHelper.WriteLine($"Order {order.Id} successfully processed!");
109-
cts.Cancel(); // Cancel the consumer after successful processing
110-
return order;
111-
}
112-
}
113-
catch (OrderNotFoundException)
114-
{
115-
// Continue polling until timeout
116-
TestOutputHelper.WriteLine($"Order {expectedOrderId} not found yet, continuing to poll...");
117-
}
118-
119-
await Task.Delay(pollInterval, TestContext.Current.CancellationToken);
120-
}
121-
122-
throw new TimeoutException($"Order {expectedOrderId} was not processed within the expected timeout.");
123-
}
124118

125119
private async Task StartOrderEventConsumerAsync(CancellationToken cancellationToken)
126120
{

0 commit comments

Comments
 (0)