使用事件源时,测试聚合要比对当前状态的存储进行测试更简单。聚合的输入是事件,聚合的输出是事件:
Given a stream of events When a valid command is performed Then new event(s) occurs
|
通常,将聚合用作建模我的域的一种方式。聚合是公开命令的地方,如果调用/调用了这些命令,将导致创建新事件。这个一般概念是事件源中的测试如何变得简单。
为了使用聚合,您首先需要从事件存储中提取所有现有事件,在聚合中重播它们以获取当前状态,然后将聚合返回给调用方。为此,我通常使用一个存储库来完成这项工作并构建聚合。
为了说明这一点,我们有使用存储库获取聚合的客户端代码。存储库调用事件存储以获取此特定聚合的现有事件(如果有)。
此时,存储库将创建一个空的聚合,并重播它从事件存储中收到的所有事件。
一旦重建了聚合,它就会将其返回给客户端。此时客户端可能会调用聚合中的各种方法/命令。
建立事件
现在,client代码访问聚合:它将可能在聚合上执行一个或多个命令。
如果客户端在聚合上调用ShipProduct()命令,并且聚合处于这样做的有效状态并传递所有不变量,则它将创建一个新事件,该事件将在内部保留在其中。
如果客户端代码随后调用了另一个命令,则另一个事件将被附加到内部列表中。
这是流程的第二阶段,我们创建了新事件,这些事件是在聚合上调用命令的结果。请记住,此第二阶段是事件源测试的“何时”阶段。
保存事件
最后一步是获取聚合中新创建的事件,并将这些事件持久保存到事件存储中。
这意味着客户代码将回调我们的存储库,并向其传递Aggreagate。
存储库将获取新事件,并将这些事件附加到该特定聚合事件流的事件存储中。
请记住,此阶段时我们的测试中的“ Then”。
Given, When, Then
如果您按照基本的3个步骤来加载聚合,调用命令,保存新事件,则可以将其归结为:
Given a stream of events When a valid command is performed Then new event(s) occurs
|
可以将其用作测试聚合的测试策略:
using System; using System.Collections.Generic;
namespace EventSourcing.Demo { public class WarehouseProductState { public int QuantityOnHand { get; set; } }
public class WarehouseProduct : AggregateRoot { public string Sku { get; }
private readonly WarehouseProductState _warehouseProductState = new();
public WarehouseProduct(string sku) { Sku = sku; }
public override void Load(IEnumerable<IEvent> events) { foreach (var evnt in events) { Apply(evnt as dynamic); } }
public static WarehouseProduct Load(string sku, IEnumerable<IEvent> events) { var warehouseProduct = new WarehouseProduct(sku); warehouseProduct.Load(events); return warehouseProduct; }
public void ShipProduct(int quantity) { if (quantity > _warehouseProductState.QuantityOnHand) { throw new InvalidDomainException("Cannot Ship to a negative Quantity on Hand."); }
var productShipped = new ProductShipped(Sku, quantity, DateTime.UtcNow);
Apply(productShipped); Add(productShipped); }
private void Apply(ProductShipped evnt) { _warehouseProductState.QuantityOnHand -= evnt.Quantity; }
public void ReceiveProduct(int quantity) { var productReceived = new ProductReceived(Sku, quantity, DateTime.UtcNow);
Apply(productReceived); Add(productReceived); }
private void Apply(ProductReceived evnt) { _warehouseProductState.QuantityOnHand += evnt.Quantity; }
public void AdjustInventory(int quantity, string reason) { if (_warehouseProductState.QuantityOnHand + quantity < 0) { throw new InvalidDomainException("Cannot adjust to a negative Quantity on Hand."); }
var inventoryAdjusted = new InventoryAdjusted(Sku, quantity, reason, DateTime.UtcNow);
Apply(inventoryAdjusted); Add(inventoryAdjusted); }
private void Apply(InventoryAdjusted evnt) { _warehouseProductState.QuantityOnHand += evnt.Quantity; }
public WarehouseProductState GetState() { return _warehouseProductState; }
public int GetQuantityOnHand() { return _warehouseProductState.QuantityOnHand; } }
public class InvalidDomainException : Exception { public InvalidDomainException(string message) : base(message) {
} } }
|
上面的WarehouseProduct具有3个命令:ShipProduct,ReceiveProduct和AdjustInventory。如果它们传递了任何不变式,所有这些都会导致创建各自的事件。
为了说明此情况,对于ShipProduct命令,应创建一个ProductShipped事件。
public class WarehouseProductTests { private readonly string _sku; private readonly int _initialQuantity; private readonly WarehouseProduct _sut; private readonly Fixture _fixture;
public WarehouseProductTests() { _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") ); } }
|