CleanArch.DevKit.Mediator.Testing
1.0.0
See the version list below for details.
dotnet add package CleanArch.DevKit.Mediator.Testing --version 1.0.0
NuGet\Install-Package CleanArch.DevKit.Mediator.Testing -Version 1.0.0
<PackageReference Include="CleanArch.DevKit.Mediator.Testing" Version="1.0.0" />
<PackageVersion Include="CleanArch.DevKit.Mediator.Testing" Version="1.0.0" />
<PackageReference Include="CleanArch.DevKit.Mediator.Testing" />
paket add CleanArch.DevKit.Mediator.Testing --version 1.0.0
#r "nuget: CleanArch.DevKit.Mediator.Testing, 1.0.0"
#:package CleanArch.DevKit.Mediator.Testing@1.0.0
#addin nuget:?package=CleanArch.DevKit.Mediator.Testing&version=1.0.0
#tool nuget:?package=CleanArch.DevKit.Mediator.Testing&version=1.0.0
CleanArch.DevKit.Mediator.Testing
Helpers de test pour le médiateur — FakeMediator configurable + MediatorHarness pour le vrai pipeline, sans Moq ni NSubstitute.
Rôle
Deux outils complémentaires :
FakeMediator— implémentationIMediatoren mémoire pour tester du code consommateur du médiateur (endpoints, services applicatifs). Stub des réponses, captures pour assertions.MediatorHarness— harnais d'intégration qui exécute le vrai pipeline (vrais handlers + behaviors via DI) tout en capturant les requêtes, notifications et streams. Idéal pour tester un handler complet avec sa chaîne de behaviors (validation, logging, etc.).
Cohérent avec la philosophie zero-reflection du toolkit : aucun framework de mock requis.
Installation
dotnet add package CleanArch.DevKit.Mediator.Testing
Dépend uniquement de CleanArch.DevKit.Mediator. MediatorHarness requiert que le projet de test consomme aussi le package CleanArch.DevKit.Mediator (pour que le générateur produise AddMediator()).
Quand utiliser quoi
| Cas d'usage | Outil |
|---|---|
| Tester un service qui envoie des requêtes au mediator | FakeMediator |
| Tester un handler isolé du pipeline | construction directe du handler |
| Tester un handler avec ses behaviors (validation, logging, …) | MediatorHarness |
| Tester qu'un handler publie une notification | MediatorHarness + CaptureNotifications<T>() |
| Tester du code qui mock le mediator sans démarrer la DI | FakeMediator |
Modes de fonctionnement
var mediator = new FakeMediator(); // mode tolérant (défaut)
var mediator = new FakeMediator(strict: true); // mode strict
- Tolérant — toute requête sans setup retourne
default(TResponse)(Send) ou une séquence vide (CreateStream). - Strict — toute requête sans setup lève
FakeMediatorMissingSetupException.
Publish n'est jamais affecté par le mode : les notifications n'ont pas de "handler manquant", elles sont juste enregistrées.
Setup des réponses
Send avec valeur
mediator.Setup<GetOrder, Order>().Returns(new Order(1, "fixed"));
Send avec valeur calculée
mediator.Setup<GetOrder, Order>().Returns(req => new Order(req.Id, $"order-{req.Id}"));
Send qui lève une exception
mediator.Setup<GetOrder, Order>().Throws(new InvalidOperationException("boom"));
mediator.Setup<GetOrder, Order>().Throws<InvalidOperationException>();
Command (IRequest sans retour)
mediator.Setup<ShipOrder>().Completes(); // succès silencieux
mediator.Setup<ShipOrder>().Throws(new ConflictException("...")); // erreur
Stream
mediator.SetupStream<StreamOrders, Order>().Yields(o1, o2, o3);
mediator.SetupStream<StreamOrders, Order>().Yields(req => Enumerable.Range(0, req.Count).Select(...));
mediator.SetupStream<StreamOrders, Order>().Throws(new InvalidOperationException("boom"));
Plusieurs
Setuppour le même type de requête : le dernier gagne.
Assertions
// Requêtes envoyées via Send, dans l'ordre d'invocation
IReadOnlyList<ShipOrder> ships = mediator.Sent<ShipOrder>();
// Notifications publiées via Publish
IReadOnlyList<OrderShipped> events = mediator.Published<OrderShipped>();
// Stream requests dispatchés via CreateStream
IReadOnlyList<StreamOrders> streams = mediator.Streamed<StreamOrders>();
Le filtre par type est polymorphique : Sent<IRequest>() retourne toutes les requêtes envoyées, Published<INotification>() toutes les notifications, etc.
Exemple complet
public sealed class OrderShipmentService
{
private readonly IMediator _mediator;
public OrderShipmentService(IMediator mediator) => _mediator = mediator;
public async Task ShipAsync(int orderId, CancellationToken ct)
{
var order = await _mediator.Send(new GetOrder(orderId), ct);
if (order is null) return;
await _mediator.Send(new MarkShipped(orderId), ct);
await _mediator.Publish(new OrderShipped(orderId), ct);
}
}
[Fact]
public async Task Ship_PublishesOrderShipped()
{
var mediator = new FakeMediator();
mediator.Setup<GetOrder, Order?>().Returns(new Order(42, "Widget"));
mediator.Setup<MarkShipped>().Completes();
var sut = new OrderShipmentService(mediator);
await sut.ShipAsync(42, CancellationToken.None);
Assert.Single(mediator.Sent<MarkShipped>());
Assert.Single(mediator.Published<OrderShipped>());
Assert.Equal(42, mediator.Published<OrderShipped>()[0].OrderId);
}
Reset entre les tests
Les fixtures partagées (IClassFixture, ICollectionFixture) peuvent réutiliser une instance de FakeMediator en appelant Reset() entre chaque test pour vider à la fois les invocations enregistrées et les setups :
mediator.Reset();
Limites de FakeMediator
- Pas de simulation du pipeline — les
IPipelineBehavior<,>enregistrés dans le vrai mediator (Logging, Validation, etc.) ne s'exécutent pas. Pour ça →MediatorHarness. - Match exact du type de requête — un setup sur
BaseQueryn'attrape pas une requêteDerivedQuery. Configurer chaque type concret. - Pas de matching par prédicat — pour différencier deux instances de la même requête, utiliser
Returns(req => ...)qui reçoit la requête en paramètre.
MediatorHarness — tester avec le vrai pipeline
Principe
MediatorHarness construit un ServiceProvider personnalisé, exécute les vrais handlers à travers la vraie chaîne de behaviors, et capture chaque Send/Publish/CreateStream pour assertions.
await using var harness = MediatorHarness.Create(b =>
{
b.Services.AddMediator(); // vrai mediator source-généré
b.Services.AddSingleton<IOrderRepo, FakeOrderRepo>(); // tes fakes/stubs côté infra
b.Services.AddLoggingBehavior(); // les behaviors que tu veux
b.CaptureNotifications<OrderShipped>(); // opt-in par type
});
var result = await harness.Send(new ShipOrder(42));
Assert.Single(harness.Sent<ShipOrder>());
Assert.Single(harness.Published<OrderShipped>());
Assert.Equal(42, harness.Published<OrderShipped>()[0].OrderId);
API
public sealed class MediatorHarness : IAsyncDisposable, IDisposable
{
public static MediatorHarness Create(Action<MediatorHarnessBuilder> configure);
public IServiceProvider Services { get; }
public Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken ct = default);
public Task Publish<TNotification>(TNotification notification, CancellationToken ct = default) where TNotification : INotification;
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(IStreamRequest<TResponse> request, CancellationToken ct = default);
public IReadOnlyList<TRequest> Sent<TRequest>();
public IReadOnlyList<TNotification> Published<TNotification>() where TNotification : INotification;
public IReadOnlyList<TRequest> Streamed<TRequest>();
public void Reset(); // vide les enregistrements, conserve la DI
}
public sealed class MediatorHarnessBuilder
{
public IServiceCollection Services { get; }
public MediatorHarnessBuilder CaptureNotifications<TNotification>() where TNotification : INotification;
}
Capture des notifications
Les notifications ne sont enregistrées que pour les types explicitement opt-in via CaptureNotifications<T>(). Cette opt-in installe un INotificationHandler<T> dans le conteneur DI qui pousse chaque notification dans la liste de capture.
- Les notifications publiées par le test (
harness.Publish(...)) sont capturées. - Les notifications publiées depuis un handler (
_mediator.Publish(...)dans le code testé) sont aussi capturées — c'est tout l'intérêt de passer par DI. - Les notifications non opt-in ne sont pas enregistrées, mais elles s'exécutent quand même normalement (leurs vrais handlers tournent si présents).
Capture des requêtes
Toutes les requêtes envoyées via Send (depuis le test ou depuis un handler appelant _mediator.Send(...)) sont capturées automatiquement via un IPipelineBehavior<,> open-generic enregistré par le harnais.
Capture des streams
Les streams envoyés via harness.CreateStream(...) sont enregistrés au niveau du harnais. Les streams déclenchés depuis un handler ne sont pas interceptés (il n'existe pas de pipeline behavior pour les streams dans Mediator.Core).
Wiring du projet de test
Pour que AddMediator() voie les handlers du projet de test, le générateur source doit y être branché. Dans le csproj de test, déclarer :
<ItemGroup>
<PackageReference Include="CleanArch.DevKit.Mediator" />
<PackageReference Include="CleanArch.DevKit.Mediator.Testing" />
</ItemGroup>
Et ajouter dans le projet de test un fichier Mediator.cs :
namespace MyApp.Tests;
public partial class Mediator { }
Le générateur peuple cette classe à la compilation. C'est la même contrainte que dans un projet consommateur normal.
Limites de MediatorHarness
- Nécessite que le générateur source tourne dans le projet de test — voir la section Wiring ci-dessus.
- Pas de capture de stream interne au handler — Mediator.Core n'expose pas de pipeline behavior pour les streams.
- Pas de mocking partiel — pour ne stubber que certaines requêtes et exécuter les autres pour de vrai, utiliser
FakeMediatorà la place.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- CleanArch.DevKit.Mediator (>= 1.0.0)
- Microsoft.Extensions.DependencyInjection (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.