Sunday, October 30, 2011

DDD Kata, part 1 (simple domain: Invoice and LineItem)

Kata Focus
1) Object identity and equality by Id
2) Id maintained in base class (entity object)
3) Equality on properties (value object)
4) A single aggregate root
5) Associations controlled from aggregate root (read-only, unique sets)
6) Business logic verified from the aggregate root

The kata will focus 80% on ORM mechanics (such as ORM issues of identity and equality) and 20% business requirements; tests are therefore delineated as M (for Mechanics) or B (for Business requirement).

The Kata
Time goal: under 30 minutes

A. DomainEntityBase

1. M: Verify that two instances of DomainEntityBase are equal when they have the same ID value
2. M: Verify that two instances are NOT equal when they have different ID values
3. M: Verify that two instances are NOT equal when they have 0 ID values.

B. Invoice, Money, and LineItems association
1. M: Verify that Invoice is an instance of DomainEntityBase.
2. M: Verify that LineItem is an instance of DomainEntityBase.
3. M: Verify that Money's constructor accepts Amount (decimal) and Currency (string) parameters whose values match equivalent properties.
4. M: Verify that Money.Amount and Money.Currency properties are read-only
5. M: Verify that two Moneys are equal when they have the same Amount and Currency.
6. M: Verify that Invoice has a read-only collection of LineItems.
7. M: Verify that adding a LineItem to Invoice increases its count of LineItems from 0 to 1
8. M: Verify that adding the SAME LineItem (by identifier) does not increment the set of LineItems.
9. M: Verify that the bi-directional reference (LineItem.Invoice) equals the owning Invoice.
10. B: Given an existing LineItem, when I try to add a LineItem without a ProductCode,
then I am informed that I must provide a ProductCode.

Bonus (outside the 30 minute kata window)
1. B: Given an existing LineItem with Price (type Money) of Currency CDN, when I try to add a LineItem with a USD Price, then I am informed that all LineItems must share the same Currency.
2. B: Given a set of LineItems, when I check the SubTotal for the Invoice, then the SubTotal matches Sum of the Quantity of LineItems times the Price.
3. B: Given an existing LineItem, when I try to add another LineItem with the same ProductCode,
then the original LineItem for that ProductCode has its Quantity incremented by the quantity of the added item.
4. B.Given an Invoice with LineItems, when the Currency of LineItems is USD and the SubTotal > 100M, then the Discount amount equals 5% of the SubTotal.

Continue with DDD Kata part 2

*****

Test Examples
DomainEntityBase, test 1:

[Test]
public void TwoInstance_SameIdInput_AreEqual()
{
    const int id = 1325123;
    var sut1 = new DomainEntityBase { Id = id };
    var sut2 = new DomainEntityBase { Id = id };

    Assert.AreEqual(sut1, sut2);
}

DomainEntityBase, test 2:

[Test]
public void TwoInstance_DifferentIdInput_AreNotEqual()
{
    var sut1 = new DomainEntityBase { Id = 123512 };
    var sut2 = new DomainEntityBase { Id = 64236 };

    Assert.AreNotEqual(sut1, sut2);
}

DomainEntityBase, test 3:

[Test]
public void TwoInstance_ZeroIdInput_AreNotEqual()
{
    var sut1 = new DomainEntityBase { Id = 0 };
    var sut2 = new DomainEntityBase { Id = 0 };

    Assert.AreNotEqual(sut1, sut2);
}

Invoice_Money_LineItems, test 1:

[Test]
public void Constructor_NoINputs_IsInstanceOfDomainEntityBase()
{
    var sut = new Invoice();
    Assert.IsInstanceOf(typeof(DomainEntityBase), sut);
}

Invoice_Money_LineItems, test 2:

[Test]
public void Constructor_NoINputs_IsInstanceOfDomainEntityBase()
{
    var sut = new LineItem();
    Assert.IsInstanceOf(typeof(DomainEntityBase), sut);
}

Invoice_Money_LineItems, test 3:

[Test]
public void Constructor_AmountAndCurrencyInputs_MatchGetterProperties()
{
    const decimal amount = 3.25M;
    const string currency = "CDN";
    var sut = new Money(amount, currency);

    Assert.AreEqual(amount, sut.Amount);
    Assert.AreEqual(currency, sut.Currency);
}

Invoice_Money_LineItems, test 4:

[Test]
public void Constructor_AmountAndCurrencyInputs_AreReadOnly()
{
    const decimal amount = 3.25M;
    const string currency = "CDN";
    var sut = new Money(amount, currency);

    Assert.IsFalse(sut.GetType().GetProperty("Amount").CanWrite);
    Assert.IsFalse(sut.GetType().GetProperty("Currency").CanWrite);
}

Invoice_Money_LineItems, test 5:

[Test]
public void TwoInstances_SameCurrencyAndAmountInputs_AreEqual()
{
    const decimal amount = 3.25M;
    const string currency = "CDN";
    var sut1 = new Money(amount, currency);
    var sut2 = new Money(amount, currency);

    Assert.AreEqual(sut1, sut2); // use struct for default "all-class-members" equality
}

Invoice_Money_LineItems, test 6:

[Test]
public void LineItemsProperty_Getter_IsReadOnlyCollection()
{
    var sut = new Invoice();
    Assert.IsInstanceOf(typeof(IEnumerable<LineItem>), sut.LineItems);
}

Invoice_Money_LineItems, test 7:

[Test]
public void AddLineItemsMethod_LineItemInput_IncrementLineItemsCollection()
{
    var sut = new Invoice();
    Assert.AreEqual(0, sut.LineItems.Count());

    sut.AddLineItem(new LineItem { ProductCode = "aaa"});
    Assert.AreEqual(1, sut.LineItems.Count());
}

Invoice_Money_LineItems, test 8:

[Test]
public void AddLineItemsMethod_SameLineItemTwiceInput_DoesNotIncrementLineItemsCollection()
{
    var sut = new Invoice();
    var lineItem = new LineItem { Id = 3522, ProductCode = "aaa" };

    sut.AddLineItem(lineItem);
    Assert.AreEqual(1, sut.LineItems.Count());
    sut.AddLineItem(lineItem);
    Assert.AreEqual(1, sut.LineItems.Count());
}

Invoice_Money_LineItems, test 9:

[Test]
public void AddLineItemsMethod_LineItemInput_InvoicePropertyMatchesParent()
{
    var sut = new Invoice();
    var lineItem = new LineItem { Id = 3522, ProductCode = "aaa"};

    sut.AddLineItem(lineItem);

    Assert.AreEqual(lineItem.Invoice, sut);
}

Invoice_Money_LineItems, test 10:

// Given an existing LineItem, when I try to add a LineItem without a ProductCode,
// then I am informed that I must provide a ProductCode.
[Test]
[ExpectedException(typeof(InvalidLineItemException), ExpectedMessage = "You must provide a ProductCode")]
public void AddLineItemsMethod_LineItemWithoutProductCode_ThrowsException()
{
    var sut = new Invoice();
    var lineItem = new LineItem { Id = 3522 };

    sut.AddLineItem(lineItem);
}