L'Event Sourcing est un pattern architectural puissant qui gagne en popularité dans l'écosystème .NET, particulièrement pour les applications modernes basées sur les microservices. Cependant, tester efficacement une application utilisant l'Event Sourcing peut s'avérer complexe et présenter de nombreux défis. Dans cet article, nous allons explorer les meilleures pratiques et les pièges courants à éviter lors des tests d'applications Event Sourcing en C#.
Les fondamentaux de l'Event Sourcing en .NET
Avant d'aborder les tests, rappelons les concepts clés de l'Event Sourcing :
- Les événements sont immuables et représentent des faits passés
- L'état actuel est reconstruit en rejouant la séquence d'événements
- Les événements sont stockés dans un Event Store
Structure de base d'un événement
public abstract class DomainEvent
{
public Guid Id { get; }
public DateTime Timestamp { get; }
public int Version { get; }
protected DomainEvent()
{
Id = Guid.NewGuid();
Timestamp = DateTime.UtcNow;
}
}
public class OrderCreatedEvent : DomainEvent
{
public Guid OrderId { get; }
public decimal Amount { get; }
public OrderCreatedEvent(Guid orderId, decimal amount)
{
OrderId = orderId;
Amount = amount;
}
}
Les pièges courants dans les tests
1. Dépendance temporelle
Un piège fréquent est de se fier à DateTime.Now dans les tests. Utilisez plutôt une abstraction du temps :
public interface ISystemClock
{
DateTime UtcNow { get; }
}
public class SystemClock : ISystemClock
{
public DateTime UtcNow => DateTime.UtcNow;
}
// Pour les tests
public class FakeSystemClock : ISystemClock
{
private DateTime _now;
public FakeSystemClock(DateTime initialTime)
{
_now = initialTime;
}
public DateTime UtcNow => _now;
public void Advance(TimeSpan duration)
{
_now = _now.Add(duration);
}
}
2. Tests d'intégration avec l'Event Store
Utilisez un Event Store en mémoire pour les tests :
public class InMemoryEventStore : IEventStore
{
private readonly Dictionary> _events
= new Dictionary>();
public async Task SaveEvents(Guid aggregateId,
IEnumerable events, int expectedVersion)
{
if (!_events.ContainsKey(aggregateId))
{
_events.Add(aggregateId, new List());
}
foreach (var @event in events)
{
_events[aggregateId].Add(@event);
}
}
public async Task> GetEvents(Guid aggregateId)
{
if (!_events.ContainsKey(aggregateId))
{
throw new AggregateNotFoundException(aggregateId);
}
return _events[aggregateId];
}
}
Bonnes pratiques de test
1. Given-When-Then Pattern
[Fact]
public async Task CreateOrder_WithValidData_ShouldEmitOrderCreatedEvent()
{
// Given
var orderId = Guid.NewGuid();
var handler = new CreateOrderHandler(_eventStore);
var command = new CreateOrderCommand(orderId, 100m);
// When
await handler.Handle(command);
// Then
var events = await _eventStore.GetEvents(orderId);
var orderCreatedEvent = events.Single() as OrderCreatedEvent;
Assert.NotNull(orderCreatedEvent);
Assert.Equal(100m, orderCreatedEvent.Amount);
}
2. Test Fixtures réutilisables
public class OrderTestFixture : IDisposable
{
public IEventStore EventStore { get; }
public ISystemClock SystemClock { get; }
public OrderTestFixture()
{
EventStore = new InMemoryEventStore();
SystemClock = new FakeSystemClock(new DateTime(2024, 1, 1));
}
public void Dispose()
{
// Nettoyage si nécessaire
}
}
Gestion des cas particuliers
1. Concurrence et versions
[Fact]
public async Task SaveEvents_WithConcurrentModification_ShouldThrowException()
{
// Arrange
var aggregateId = Guid.NewGuid();
var eventStore = new InMemoryEventStore();
await eventStore.SaveEvents(aggregateId,
new[] { new OrderCreatedEvent(aggregateId, 100m) }, 0);
// Act & Assert
await Assert.ThrowsAsync(() =>
eventStore.SaveEvents(aggregateId,
new[] { new OrderModifiedEvent(aggregateId) }, 0));
}
2. Snapshots dans les tests
public class OrderSnapshot
{
public Guid Id { get; set; }
public decimal Amount { get; set; }
public int Version { get; set; }
}
[Fact]
public async Task LoadFromSnapshot_ShouldRestoreCorrectState()
{
// Arrange
var snapshotStore = new InMemorySnapshotStore();
var orderId = Guid.NewGuid();
var snapshot = new OrderSnapshot
{
Id = orderId,
Amount = 100m,
Version = 1
};
await snapshotStore.Save(snapshot);
// Act
var order = await Order.LoadFromSnapshot(snapshot, _eventStore);
// Assert
Assert.Equal(100m, order.Amount);
Assert.Equal(1, order.Version);
}
Optimisation des performances des tests
Pour maintenir des tests performants :
- Utilisez des collections de tests parallélisables
- Évitez les dépendances externes dans les tests unitaires
- Implémentez un mécanisme de cache pour les agrégats fréquemment utilisés
[Collection("Parallel")]
public class ParallelOrderTests
{
private readonly IEventStore _eventStore;
private readonly ISystemClock _clock;
public ParallelOrderTests()
{
_eventStore = new InMemoryEventStore();
_clock = new FakeSystemClock(DateTime.UtcNow);
}
// Tests...
}
Conclusion
Tester efficacement une application Event Sourcing requiert une approche méthodique et la compréhension des pièges courants. Les points clés à retenir :
- Utilisez des abstractions pour le temps et les dépendances externes
- Implémentez des stores en mémoire pour les tests
- Suivez le pattern Given-When-Then
- Gérez correctement la concurrence et les versions
- Optimisez les performances des tests
En suivant ces bonnes pratiques, vous pourrez construire une suite de tests robuste et maintenable pour vos applications Event Sourcing en .NET.