using System; using System.Linq; using AutoFixture; using Shouldly; using Xunit; namespace EventSourcing.Demo { public abstract class AggregateTests<TAggregate> where TAggregate : AggregateRoot { private readonly TAggregate _aggregateRoot; protected AggregateTests(TAggregate aggregateRoot) { _aggregateRoot = aggregateRoot; } protected void Given(params IEvent[] events) { if (events != null) { _aggregateRoot.Load(events); } } protected void When(Action<TAggregate> command) { command(_aggregateRoot); } protected void Then<TEvent>(params Action<TEvent>[] conditions) { var events = _aggregateRoot.GetUncommittedEvents(); events.Count.ShouldBe(1); var evnt = events.First(); evnt.ShouldBeOfType<TEvent>(); if (conditions != null) { ((TEvent)evnt).ShouldSatisfyAllConditions(conditions); } } protected void Throws<TException>(Action<TAggregate> command, params Action<TException>[] conditions) where TException : Exception { var ex = Should.Throw<TException>(() => command(_aggregateRoot)); if (conditions != null) { ex.ShouldSatisfyAllConditions(conditions); } } } public class GivenWhenThenTests : AggregateTests<WarehouseProduct> { private readonly Fixture _fixture; private readonly string _sku = "abc123"; private readonly int _initialQuantity; public GivenWhenThenTests() : base(new WarehouseProduct("abc123")) { _fixture = new Fixture(); _fixture.Customizations.Add(new Int32SequenceGenerator()); _initialQuantity = (int)_fixture.Create<uint>(); } [Fact] public void ShipProductShouldRaiseProductShipped() { Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow)); var quantityToShip = _fixture.Create<int>(); When(x => x.ShipProduct(quantityToShip)); Then<ProductShipped>( x => x.Quantity.ShouldBe(quantityToShip), x => x.Sku.ShouldBe(_sku), x => x.EventType.ShouldBe("ProductShipped")); } [Fact] public void ShipProductShouldThrowIfNoQuantityOnHand() { Given(); Throws<InvalidDomainException>( x => x.ShipProduct(1), x => x.Message.ShouldBe("Cannot Ship to a negative Quantity on Hand.")); } [Fact] public void ReceiveProductShouldRaiseProductReceived() { Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow)); var quantityToReceive = _fixture.Create<int>(); When(x => x.ReceiveProduct(quantityToReceive)); Then<ProductReceived>( x => x.Quantity.ShouldBe(quantityToReceive), x => x.Sku.ShouldBe(_sku), x => x.EventType.ShouldBe("ProductReceived")); } [Fact] public void AdjustInventoryShouldRaiseProductAdjusted() { Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow)); var quantityToAdjust = _fixture.Create<int>(); var reason = _fixture.Create<string>(); When(x => x.AdjustInventory(quantityToAdjust, reason)); Then<InventoryAdjusted>( x => x.Quantity.ShouldBe(quantityToAdjust), x => x.Sku.ShouldBe(_sku), x => x.Reason.ShouldBe(reason), x => x.EventType.ShouldBe("InventoryAdjusted")); } [Fact] public void AdjustInventoryShouldThrowIfNoQuantityOnHand() { Given(); var reason = _fixture.Create<string>(); Throws<InvalidDomainException>( x => x.AdjustInventory(-1, reason), x => x.Message.ShouldBe("Cannot adjust to a negative Quantity on Hand.")); } } public class ProductTests { private readonly string _sku; private readonly int _initialQuantity; private readonly WarehouseProduct _sut; private readonly Fixture _fixture; public ProductTests() { _fixture = new Fixture(); _fixture.Customizations.Add(new Int32SequenceGenerator()); _sku = _fixture.Create<string>(); _initialQuantity = (int)_fixture.Create<uint>(); _sut = WarehouseProduct.Load(_sku, new [] { new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow) }); } [Fact] public void ShipProductShouldRaiseProductShipped() { var quantityToShip = _fixture.Create<int>(); _sut.ShipProduct(quantityToShip); var outEvents = _sut.GetUncommittedEvents(); outEvents.Count.ShouldBe(1); var outEvent = outEvents.Single(); outEvent.ShouldBeOfType<ProductShipped>(); var productShipped = (ProductShipped)outEvent; productShipped.ShouldSatisfyAllConditions( x => x.Quantity.ShouldBe(quantityToShip), x => x.Sku.ShouldBe(_sku), x => x.EventType.ShouldBe("ProductShipped") ); } [Fact] public void ShipProductShouldThrowIfNoQuantityOnHand() { var ex = Should.Throw<InvalidDomainException>(() => _sut.ShipProduct(_initialQuantity + 1)); ex.Message.ShouldBe("Cannot Ship to a negative Quantity on Hand."); } [Fact] public void ReceiveProductShouldRaiseProductReceived() { var quantityToReceive = _fixture.Create<int>(); _sut.ReceiveProduct(quantityToReceive); var outEvents = _sut.GetUncommittedEvents(); outEvents.Count.ShouldBe(1); var outEvent = outEvents.Single(); outEvent.ShouldBeOfType<ProductReceived>(); var productReceived = (ProductReceived)outEvent; productReceived.ShouldSatisfyAllConditions( x => x.Quantity.ShouldBe(quantityToReceive), x => x.Sku.ShouldBe(_sku), x => x.EventType.ShouldBe("ProductReceived") ); } [Fact] public void AdjustInventoryShouldRaiseProductAdjusted() { var quantityToAdjust = _fixture.Create<int>(); var reason = _fixture.Create<string>(); _sut.AdjustInventory(quantityToAdjust, reason); var outEvents = _sut.GetUncommittedEvents(); outEvents.Count.ShouldBe(1); var outEvent = outEvents.Single(); outEvent.ShouldBeOfType<InventoryAdjusted>(); var productShipped = (InventoryAdjusted)outEvent; productShipped.ShouldSatisfyAllConditions( x => x.Quantity.ShouldBe(quantityToAdjust), x => x.Sku.ShouldBe(_sku), x => x.Reason.ShouldBe(reason), x => x.EventType.ShouldBe("InventoryAdjusted") ); } [Fact] public void AdjustInventoryShouldThrowIfNoQuantityOnHand() { var ex = Should.Throw<InvalidDomainException>(() => _sut.AdjustInventory((_initialQuantity + 1) * -1, string.Empty)); ex.Message.ShouldBe("Cannot adjust to a negative Quantity on Hand."); } } }