Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
231628e
Add MockEndpoint and AspireIntegrationTestHost infrastructure for E2E…
Copilot Apr 6, 2026
f64a429
Rewrite Tutorial01-05 tests to use MockEndpoint E2E patterns
Copilot Apr 6, 2026
ff08ef7
Rewrite Tutorial06-10 tests to use real channel classes with MockEndp…
Copilot Apr 6, 2026
3c7d3aa
Rewrite Tutorial 11-15 Lab.cs and Exam.cs with E2E MockEndpoint pattern
Copilot Apr 6, 2026
887c5a7
Rewrite Tutorial16-20 Lab.cs and Exam.cs with E2E tests
Copilot Apr 6, 2026
8c86068
Rewrite Tutorial21-25 Lab.cs and Exam.cs with E2E MockEndpoint pattern
Copilot Apr 6, 2026
6964a47
Rewrite Tutorial 26-30 Lab.cs and Exam.cs with E2E tests
Copilot Apr 6, 2026
5b810c4
Rewrite Tutorial 31-35 Lab/Exam to E2E MockEndpoint integration pattern
Copilot Apr 6, 2026
82ee841
Rewrite Tutorial 36-40 Lab.cs and Exam.cs to use E2E MockEndpoint pat…
Copilot Apr 6, 2026
6383fd9
Rewrite Tutorial41-45 Lab.cs and Exam.cs to use E2E MockEndpoint pattern
Copilot Apr 6, 2026
03cd4fb
Rewrite tutorials 46-50 with E2E MockEndpoint integration (47 tests)
Copilot Apr 6, 2026
33f3660
Create src/Testing library with real mock implementations for all 19 …
Copilot Apr 6, 2026
1540f91
refactor(Tutorial07): replace NSubstitute with MockTemporalWorkflowDi…
Copilot Apr 6, 2026
f14ea4b
refactor(Tutorial14): replace NSubstitute with MockTemporalWorkflowDi…
Copilot Apr 6, 2026
262a023
Replace NSubstitute with mock implementations in Tutorial08, Tutorial…
Copilot Apr 6, 2026
b44110e
Replace NSubstitute with mock implementations from Testing library in…
Copilot Apr 6, 2026
a0ece51
Replace NSubstitute with mock implementations in Tutorial36, 37, 38
Copilot Apr 6, 2026
bd2ab31
Replace NSubstitute with mock implementations from EnterpriseIntegrat…
Copilot Apr 6, 2026
f4e0969
Replace NSubstitute with mock implementations from EnterpriseIntegrat…
Copilot Apr 6, 2026
ab544ea
Rewrite all 31 NSubstitute files to use src/Testing library, remove N…
Copilot Apr 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions EnterpriseIntegrationPlatform/EnterpriseIntegrationPlatform.sln
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processing.RequestReply", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors", "src\Connectors\Connectors.csproj", "{7998C735-EB8F-4DBE-BB32-978E9A465433}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testing", "src\Testing\Testing.csproj", "{F13607C8-980A-4EFF-93B5-5D6FE344F08C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -751,6 +753,18 @@ Global
{7998C735-EB8F-4DBE-BB32-978E9A465433}.Release|x64.Build.0 = Release|Any CPU
{7998C735-EB8F-4DBE-BB32-978E9A465433}.Release|x86.ActiveCfg = Release|Any CPU
{7998C735-EB8F-4DBE-BB32-978E9A465433}.Release|x86.Build.0 = Release|Any CPU
{F13607C8-980A-4EFF-93B5-5D6FE344F08C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F13607C8-980A-4EFF-93B5-5D6FE344F08C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F13607C8-980A-4EFF-93B5-5D6FE344F08C}.Debug|x64.ActiveCfg = Debug|Any CPU
{F13607C8-980A-4EFF-93B5-5D6FE344F08C}.Debug|x64.Build.0 = Debug|Any CPU
{F13607C8-980A-4EFF-93B5-5D6FE344F08C}.Debug|x86.ActiveCfg = Debug|Any CPU
{F13607C8-980A-4EFF-93B5-5D6FE344F08C}.Debug|x86.Build.0 = Debug|Any CPU
{F13607C8-980A-4EFF-93B5-5D6FE344F08C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F13607C8-980A-4EFF-93B5-5D6FE344F08C}.Release|Any CPU.Build.0 = Release|Any CPU
{F13607C8-980A-4EFF-93B5-5D6FE344F08C}.Release|x64.ActiveCfg = Release|Any CPU
{F13607C8-980A-4EFF-93B5-5D6FE344F08C}.Release|x64.Build.0 = Release|Any CPU
{F13607C8-980A-4EFF-93B5-5D6FE344F08C}.Release|x86.ActiveCfg = Release|Any CPU
{F13607C8-980A-4EFF-93B5-5D6FE344F08C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -811,5 +825,6 @@ Global
{F7FBDC14-6ED2-46AC-B348-2427C14F0158} = {A1B2C3D4-0001-0001-0001-000000000002}
{F8DD5966-EE52-4ADA-BE4F-D23636F424F8} = {A1B2C3D4-0001-0001-0001-000000000002}
{7998C735-EB8F-4DBE-BB32-978E9A465433} = {A1B2C3D4-0001-0001-0001-000000000002}
{F13607C8-980A-4EFF-93B5-5D6FE344F08C} = {A1B2C3D4-0001-0001-0001-000000000001}
EndGlobalSection
EndGlobal
103 changes: 103 additions & 0 deletions EnterpriseIntegrationPlatform/src/Testing/AspireIntegrationTestHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// ============================================================================
// AspireIntegrationTestHost – DI-based test host for E2E integration testing
// ============================================================================

using EnterpriseIntegrationPlatform.Ingestion;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace EnterpriseIntegrationPlatform.Testing;

/// <summary>
/// Aspire-style integration test host that wires real EIP components
/// with MockEndpoints via dependency injection.
/// </summary>
public sealed class AspireIntegrationTestHost : IAsyncDisposable
{
private readonly IHost _host;
private readonly Dictionary<string, MockEndpoint> _endpoints;

internal AspireIntegrationTestHost(IHost host, Dictionary<string, MockEndpoint> endpoints)
{
_host = host;
_endpoints = endpoints;
}

public IServiceProvider Services => _host.Services;

public T GetService<T>() where T : notnull =>
_host.Services.GetRequiredService<T>();

public MockEndpoint GetEndpoint(string name) => _endpoints[name];

public IReadOnlyDictionary<string, MockEndpoint> Endpoints => _endpoints;

public static Builder CreateBuilder() => new();

public ValueTask DisposeAsync()
{
_host.Dispose();
return ValueTask.CompletedTask;
}

public sealed class Builder
{
private readonly HostApplicationBuilder _inner;
private readonly Dictionary<string, MockEndpoint> _endpoints = new();

public Builder()
{
_inner = Host.CreateApplicationBuilder([]);
_inner.Services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
_inner.Services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
}

/// <summary>Adds a named MockEndpoint for testing.</summary>
public MockEndpoint AddMockEndpoint(string name)
{
var ep = new MockEndpoint(name);
_endpoints[name] = ep;
return ep;
}

/// <summary>Registers a MockEndpoint as the default IMessageBrokerProducer.</summary>
public Builder UseProducer(MockEndpoint endpoint)
{
_inner.Services.AddSingleton<IMessageBrokerProducer>(endpoint);
return this;
}

/// <summary>Registers a MockEndpoint as the default IMessageBrokerConsumer.</summary>
public Builder UseConsumer(MockEndpoint endpoint)
{
_inner.Services.AddSingleton<IMessageBrokerConsumer>(endpoint);
return this;
}

public Builder ConfigureServices(Action<IServiceCollection> configure)
{
configure(_inner.Services);
return this;
}

public Builder AddSingleton<TService>(TService instance) where TService : class
{
_inner.Services.AddSingleton(instance);
return this;
}

public Builder Configure<TOptions>(Action<TOptions> configure) where TOptions : class
{
_inner.Services.Configure(configure);
return this;
}

public AspireIntegrationTestHost Build()
{
var host = _inner.Build();
return new AspireIntegrationTestHost(host, _endpoints);
}
}
}
172 changes: 172 additions & 0 deletions EnterpriseIntegrationPlatform/src/Testing/MockActivityServices.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// ============================================================================
// MockActivityServices – In-memory activity services for testing
// ============================================================================

using System.Collections.Concurrent;
using EnterpriseIntegrationPlatform.Activities;

namespace EnterpriseIntegrationPlatform.Testing;

/// <summary>
/// Real in-memory implementation of <see cref="ICompensationActivityService"/>
/// that returns configurable results per step name.
/// </summary>
public sealed class MockCompensationActivityService : ICompensationActivityService
{
private readonly Dictionary<string, bool> _stepResults = new();
private readonly ConcurrentQueue<CompensationCallRecord> _calls = new();
private bool _defaultResult = true;

/// <summary>All compensation calls recorded.</summary>
public IReadOnlyList<CompensationCallRecord> Calls => _calls.ToArray();

/// <summary>Sets the result for a specific step name.</summary>
public MockCompensationActivityService WithStepResult(string stepName, bool success)
{
_stepResults[stepName] = success;
return this;
}

/// <summary>Sets the default result for unmatched steps.</summary>
public MockCompensationActivityService WithDefaultResult(bool success)
{
_defaultResult = success;
return this;
}

public Task<bool> CompensateAsync(Guid correlationId, string stepName)
{
_calls.Enqueue(new CompensationCallRecord(correlationId, stepName));
var result = _stepResults.TryGetValue(stepName, out var r) ? r : _defaultResult;
return Task.FromResult(result);
}

public sealed record CompensationCallRecord(Guid CorrelationId, string StepName);
}

/// <summary>
/// Real in-memory implementation of <see cref="IMessageValidationService"/>
/// that returns configurable validation results per message type.
/// </summary>
public sealed class MockMessageValidationService : IMessageValidationService
{
private readonly Dictionary<string, MessageValidationResult> _results = new();
private readonly ConcurrentQueue<ValidationCallRecord> _calls = new();
private MessageValidationResult _defaultResult = MessageValidationResult.Success;

/// <summary>All validation calls recorded.</summary>
public IReadOnlyList<ValidationCallRecord> Calls => _calls.ToArray();

/// <summary>Sets the result for a specific message type.</summary>
public MockMessageValidationService WithResult(string messageType, MessageValidationResult result)
{
_results[messageType] = result;
return this;
}

/// <summary>Sets the default result for unmatched types.</summary>
public MockMessageValidationService WithDefaultResult(MessageValidationResult result)
{
_defaultResult = result;
return this;
}

public Task<MessageValidationResult> ValidateAsync(string messageType, string payloadJson)
{
_calls.Enqueue(new ValidationCallRecord(messageType, payloadJson));
var result = _results.TryGetValue(messageType, out var r) ? r : _defaultResult;
return Task.FromResult(result);
}

public sealed record ValidationCallRecord(string MessageType, string PayloadJson);
}

/// <summary>
/// Real in-memory implementation of <see cref="IPersistenceActivityService"/>
/// that captures all persistence calls.
/// </summary>
public sealed class MockPersistenceActivityService : IPersistenceActivityService
{
private readonly ConcurrentQueue<PersistenceCallRecord> _calls = new();

/// <summary>All persistence calls recorded.</summary>
public IReadOnlyList<PersistenceCallRecord> Calls => _calls.ToArray();

/// <summary>Number of SaveMessage calls.</summary>
public int SaveCount => _calls.Count(c => c.Operation == "SaveMessage");

/// <summary>Number of UpdateDeliveryStatus calls.</summary>
public int UpdateStatusCount => _calls.Count(c => c.Operation == "UpdateDeliveryStatus");

/// <summary>Number of SaveFault calls.</summary>
public int SaveFaultCount => _calls.Count(c => c.Operation == "SaveFault");

public Task SaveMessageAsync(IntegrationPipelineInput input, CancellationToken cancellationToken = default)
{
_calls.Enqueue(new PersistenceCallRecord("SaveMessage", input.MessageId, input.MessageType, null));
return Task.CompletedTask;
}

public Task UpdateDeliveryStatusAsync(
Guid messageId, Guid correlationId, DateTimeOffset recordedAt,
string status, CancellationToken cancellationToken = default)
{
_calls.Enqueue(new PersistenceCallRecord("UpdateDeliveryStatus", messageId, status, null));
return Task.CompletedTask;
}

public Task SaveFaultAsync(
Guid messageId, Guid correlationId, string messageType,
string faultedBy, string reason, int retryCount,
CancellationToken cancellationToken = default)
{
_calls.Enqueue(new PersistenceCallRecord("SaveFault", messageId, messageType, reason));
return Task.CompletedTask;
}

/// <summary>Asserts that SaveMessage was called the expected number of times.</summary>
public void AssertSaveCount(int expected) =>
NUnit.Framework.Assert.That(SaveCount, NUnit.Framework.Is.EqualTo(expected));

public void Reset()
{
while (_calls.TryDequeue(out _)) { }
}

public sealed record PersistenceCallRecord(string Operation, Guid MessageId, string? Detail, string? Reason);
}

/// <summary>
/// Real in-memory implementation of <see cref="IMessageLoggingService"/>
/// that captures all log entries.
/// </summary>
public sealed class MockMessageLoggingService : IMessageLoggingService
{
private readonly ConcurrentQueue<LogRecord> _logs = new();

/// <summary>All log entries recorded.</summary>
public IReadOnlyList<LogRecord> Logs => _logs.ToArray();

/// <summary>Number of log entries.</summary>
public int LogCount => _logs.Count;

public Task LogAsync(Guid messageId, string messageType, string stage)
{
_logs.Enqueue(new LogRecord(messageId, messageType, stage));
return Task.CompletedTask;
}

/// <summary>Asserts a specific stage was logged for the given message.</summary>
public void AssertLogged(Guid messageId, string stage) =>
NUnit.Framework.Assert.That(
_logs.Any(l => l.MessageId == messageId && l.Stage == stage),
NUnit.Framework.Is.True,
$"Expected log entry for message {messageId} at stage '{stage}'");

public void Reset()
{
while (_logs.TryDequeue(out _)) { }
}

public sealed record LogRecord(Guid MessageId, string MessageType, string Stage);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// ============================================================================
// MockAggregationStrategy – Configurable aggregation for testing
// ============================================================================

using EnterpriseIntegrationPlatform.Processing.Aggregator;

namespace EnterpriseIntegrationPlatform.Testing;

/// <summary>
/// Real in-memory implementation of <see cref="IAggregationStrategy{TItem,TAggregate}"/>
/// that applies a configurable aggregation function.
/// </summary>
public sealed class MockAggregationStrategy<TItem, TAggregate> : IAggregationStrategy<TItem, TAggregate>
{
private readonly Func<IReadOnlyList<TItem>, TAggregate> _aggregateFunc;
private int _callCount;

/// <summary>Creates a mock strategy with the given aggregation function.</summary>
public MockAggregationStrategy(Func<IReadOnlyList<TItem>, TAggregate> aggregateFunc) =>
_aggregateFunc = aggregateFunc;

/// <summary>Number of aggregation calls.</summary>
public int CallCount => _callCount;

public TAggregate Aggregate(IReadOnlyList<TItem> items)
{
Interlocked.Increment(ref _callCount);
return _aggregateFunc(items);
}
}
Loading
Loading