Newer
Older
EventSourcingTesting / Test.cs
@Derek Comartin Derek Comartin on 29 Apr 2021 7 KB Given/When/Then for simpler testing
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.");
        }
    }
}