Saturday, February 06, 2010

Day 3 of 5 Day TDD Kata: "Invoice Applies Taxes to Items via ITaxesService"

I've just completed Day 3 of the 5 Day TDD Kata.

The project is growing into multiple tests and classes, so I will only post a brief code sample below; full samples are posted to github here.

Here's the kata:

DAY 3: INVOICE APPLIES TAXES TO ITEMS VIA ITaxesService
  1. Add new instance variable to Tax: int Percent. Add Percent to constructor. Include percent in instance equality. Fix all tests. Add test to reject 0 percent as constructor parameter.
  2. Create InvoiceTests class.
  3. In Setup, create ITaxesService stub with taxes from 3 jurisdictions.
  4. Inject ITaxesService into Invoice constructor. Validate that Invoice.Taxes equals ITaxesService.Taxes.
  5. Create InvoiceItemTests. Verify that InvoiceItem is constructed with quantity and amount parameters, values shoudl match properties InvoiceItem.Quantity and InvoiceItem.Amount. (Product and/or Description are outside scope of this kata.)
  6. Submitting 0 to quantity or amount throws exception, but object equality is not required. 
  7. Return to InvoiceTests. Validate that when Invoice adds InvoiceItems, that TotalItemQuantity matches sum of Items quantity.
  8. Invoice SubTotal matches sum of (Quantity * Amount)
  9. Invoice has multiple TaxCalculations, each has Tax and Amount properties. Validate that each TaxCalculation.Amount = SubTotal * Tax.Percent / 100 (eg. 20.00 * 5 * .01 = 1.00).
  10. Invoice Total matches Invoice.SubTotal + sum of (Invoice.TaxCalculations).

InvoiceItemTests.cs

[TestFixture]
public class InvoiceItemTests
{
    [Test]
    public void InvoiceItemParametersMatchProperties()
    {
        const int quantity = 3;
        const decimal amount = 15.25M;
        var invoiceLineItem = new InvoiceItem(quantity, amount);

        Assert.AreEqual(quantity, invoiceLineItem.Quantity);
        Assert.AreEqual(amount, invoiceLineItem.Amount);
    }

    [Test]
    [ExpectedException(typeof(NullOrZeroConstructorParameterException))]
    public void InvoiceItemRequiresNonZeroQuantittOnCreation()
    {
        const int quantity = 0;
        const decimal amount = 15.25M;
        var invoiceLineItem = new InvoiceItem(quantity, amount);
    }

    [Test]
    [ExpectedException(typeof(NullOrZeroConstructorParameterException))]
    public void InvoiceItemRequiresNonZeroAmountOnCreation()
    {
        const int quantity = 4;
        const decimal amount = 0M;
        var invoiceLineItem = new InvoiceItem(quantity, amount);
    }

}


InvoiceTests.cs

[TestFixture]
public class InvoiceTests
{
    private MockRepository _mockRepository;
    private ITaxesService _mockTaxesService;
    private City _city;
    private ProvinceState _provinceState;
    private Country _country;

    [SetUp]
    public void Setup()
    {
        _mockRepository = new MockRepository();
        _mockTaxesService = _mockRepository.Stub();

        _city = new City("CityTax", _mockTaxesService);
        var cityTax = new Tax("CityTax", DateTime.Today, DateTime.Today.AddMonths(6), JurisdictionEnum.City, 2);
        _mockTaxesService.Taxes.Add(cityTax);

        _provinceState = new ProvinceState("ProvStateTax", _mockTaxesService);
        var provStateTax = new Tax("ProvStateTax", DateTime.Today, DateTime.Today.AddMonths(6), JurisdictionEnum.ProvinceState, 3);
        _mockTaxesService.Taxes.Add(provStateTax);

        _country = new Country("CountryTax", _mockTaxesService);
        var countryTax = new Tax("CountryTax", DateTime.Today, DateTime.Today.AddMonths(6), JurisdictionEnum.Country, 4);
        _mockTaxesService.Taxes.Add(countryTax);
    }

    [Test]
    public void InvoiceGetsTaxesFromITaxesService()
    {
        var invoice = new Invoice(_mockTaxesService);
        Assert.AreEqual(invoice.Taxes, _mockTaxesService.Taxes);
    }

    [Test]
    public void InvoiceSumOfInvoiceLineItemQtyMatchesTotalItemQuantity()
    {
        var invoice = GetInvoice();

        Assert.AreEqual(12, invoice.TotalItemQuantity);
    }

    [Test]
    public void InvoiceSubtotalEqualsSumOfItemQuantityTimesAmount()
    {
        var invoice = GetInvoice();

        Assert.AreEqual(95.00M, invoice.SubTotal);
    }

    [Test]
    public void InvoiceHasMultipleTaxCalcRowsWithTypeAndTaxAmount()
    {
        var invoice = GetInvoice();

        foreach (var taxCalculation in invoice.TaxCalculations)
        {
            var expectedAmount = invoice.SubTotal * taxCalculation.Tax.Percent * .01M;

             switch (taxCalculation.Tax.Jurisdiction)
            {
                case JurisdictionEnum.City:
                    Assert.AreEqual(expectedAmount, taxCalculation.Amount);
                    break;
                case JurisdictionEnum.ProvinceState:
                    Assert.AreEqual(expectedAmount, taxCalculation.Amount);
                    break;
                case JurisdictionEnum.Country:
                    Assert.AreEqual(expectedAmount, taxCalculation.Amount);
                    break;
            }
        }
    }

    [Test]
    public void InvoiceTotalIsSubTotalPlusSumOfTaxCalculations()
    {
        var invoice = GetInvoice();

        var expectedAmount = invoice.SubTotal;

        foreach (var taxCalculation in invoice.TaxCalculations)
            expectedAmount += taxCalculation.Amount;

        Assert.AreEqual(expectedAmount, invoice.Total);
    }

    private Invoice GetInvoice()
    {
        var invoice = new Invoice(_mockTaxesService);
        invoice.AddLineItem(new InvoiceItem(3, 15.00M));
        invoice.AddLineItem(new InvoiceItem(4, 10.00M));
        invoice.AddLineItem(new InvoiceItem(5, 2.00M));
        return invoice;
    }

}

No comments: