Monday, February 08, 2010

Day 5 of 5 Day TDD Kata: "Invoice Presenter Adds and Displays Invoice with Taxes and Totals"

And that's it: the 5 Day TDD Kata is now complete!

As before, I will only post a brief code sample below; full samples are posted to github here.

Here's the kata:

DAY 5: InvoicePresenter ADDS AND DISPLAYS INVOICE WITH TAXES AND TOTALS
      The culmination. This kata works with everything you have built leading up to it.
  1. Create a namespace for Repository.
  2. Create CustomerTests. Verify when Customer adds an Invoice, that Customer.Invoices increments.
  3. Create InvoicePresenterTests with 4 mocked interfaces: ITaxesRepository, ICustomerRepository, IInvoiceRepository, and IInvoiceView.
  4. Create the following IInvoiceView events, each with a default EventHandler:
    • GetCustomer event
    • AddInvoiceLine event
    • CalculateTotals event
    • SaveInvoice event 
  5. For each one, verify that InvoicePresenter's constructor attaches that event to an event handler. (You can expect each event to be assigned to null, and instruct the mock to ignore the argument. For subsequent tests, you only need to get the event if the current test needs to verify behaviour that occurs when the event is raised.)
  6. When GetCustomer event is raised, verify that
    a) IInvoiceView.CustomerCode is assigned to ICustomerRepository.FindCustomerByCode().
    b) the returned Customer is retrieved, and FirstName and LastName are assigned to IInvoiceView.FirstName and IInvoiceView.LastName. 
  7. When AddInvoiceLine event is raised, verify that:
    a) IInvoiceView.Quantity and IInvoiceView.Amount are retrieved and assigned to InvoiceItem constructor.
    b) ITaxesRepository.GetTaxesService() is called and returns ITaxesService.
    c) Invoice is instantiated with ITaxesService, and InvoiceItem is added to Invoice.AddLineItem()
    d) Invoice.LineItems is assigned to IInvoiceView.InvoiceLineItems property.
    e) When the last expectation fails, add equality checking (IsEqual() and GetHashCode()) to InvoiceItem class, based on properties of InvoiceItem (quantity and amount.)
  8. When CalculateTotals event is raised, verify that:
    a) Invoice.SubTotal is assigned to IInvoiceView.Subtotal.
    b) Invoice.TaxCalculations are assigned to IInvoiceView.TaxCalculations.
    c) Invoice.Total is assigned to IInvoiceView.Total. 
  9. When SaveInvoice event is raised, verify that:
    a) IInvoiceRepository.SaveInvoice() is passed the current Invoice instance.
    b) When this expecation fails, add equality checking (IsEqual() and GetHashCode() to Invoice, based on 0 items equal, or equality of items.

InvoicePresenterTests.cs

[TestFixture]
public class InvoicePresenterTests
{
    private MockRepository _mockRepository;
    private ICustomerRepository _mockCustomerRepository;
    private ITaxesRepository _mockTaxesRepository;
    private IInvoiceRepository _mockInvoiceRepository;
    private IInvoiceView _mockInvoiceView;

    [SetUp]
    public void SetUp()
    {
        _mockRepository = new MockRepository();
        _mockCustomerRepository = _mockRepository.StrictMock();
        _mockTaxesRepository = _mockRepository.StrictMock();
        _mockInvoiceRepository = _mockRepository.StrictMock();
        _mockInvoiceView = _mockRepository.StrictMock();
    }

    [TearDown]
    public void TearDown()
    {
        _mockRepository.VerifyAll();
    }

    [Test]
    public void InvoicePresenterAttachesAllViewEvents()
    {
        _mockInvoiceView.GetCustomer += null;
        LastCall.IgnoreArguments();
        _mockInvoiceView.AddInvoiceLine += null;
        LastCall.IgnoreArguments();
        _mockInvoiceView.CalculateTotals += null;
        LastCall.IgnoreArguments();
        _mockInvoiceView.SaveInvoice += null;
        LastCall.IgnoreArguments();

        _mockRepository.ReplayAll();

        var invoicePresenter = new InvoicePresenter(_mockCustomerRepository, _mockTaxesRepository, _mockInvoiceRepository, _mockInvoiceView);
    }

    [Test]
    public void InvoiceGetCustomerEventRetrievesCustomerInformation()
    {
        _mockInvoiceView.GetCustomer += null;
        var getCustomerEventRaiser = LastCall.IgnoreArguments().GetEventRaiser();
        _mockInvoiceView.AddInvoiceLine += null;
        LastCall.IgnoreArguments();
        _mockInvoiceView.CalculateTotals += null;
        LastCall.IgnoreArguments();
        _mockInvoiceView.SaveInvoice += null;
        LastCall.IgnoreArguments();

        const string customerCode = "JIMSMI";
        var customer = new Customer();
        Expect.Call(_mockInvoiceView.CustomerCode).Return(customerCode);
        Expect.Call(_mockCustomerRepository.FindCustomerByCode(customerCode)).Return(customer);
        _mockInvoiceView.CustomerFirstName = customer.FirstName;
        _mockInvoiceView.CustomerLastName = customer.LastName;

        _mockRepository.ReplayAll();

        var invoicePresenter = new InvoicePresenter(_mockCustomerRepository, _mockTaxesRepository, _mockInvoiceRepository, _mockInvoiceView);
        getCustomerEventRaiser.Raise(_mockInvoiceView, EventArgs.Empty);
    }

    [Test]
    public void InvoiceAddLineItemEventAddsLineItem()
    {
        _mockInvoiceView.GetCustomer += null;
        LastCall.IgnoreArguments();
        _mockInvoiceView.AddInvoiceLine += null;
        var addInvoiceLineEventRaiser = LastCall.IgnoreArguments().GetEventRaiser();
        _mockInvoiceView.CalculateTotals += null;
        LastCall.IgnoreArguments();
        _mockInvoiceView.SaveInvoice += null;
        LastCall.IgnoreArguments();

        const int quantity = 3;
        const decimal amount = 35.00M;
        ITaxesService taxesService = new TaxesService();
        Expect.Call(_mockInvoiceView.Quantity).Return(quantity);
        Expect.Call(_mockInvoiceView.Amount).Return(amount);
        Expect.Call(_mockTaxesRepository.GetTaxesService()).Return(taxesService);
        var invoiceItem = new InvoiceItem(quantity, amount);
        var invoice = new Invoice(taxesService);
        invoice.AddLineItem(invoiceItem);
        _mockInvoiceView.InvoiceLineItems = invoice.InvoiceItems;

        _mockRepository.ReplayAll();

        var invoicePresenter = new InvoicePresenter(_mockCustomerRepository, _mockTaxesRepository, _mockInvoiceRepository, _mockInvoiceView);
        addInvoiceLineEventRaiser.Raise(_mockInvoiceView, EventArgs.Empty);
    }

    [Test]
    public void InvoiceCalculateTotalsEventDisplaysSubTotalTaxesAndTotal()
    {
        _mockInvoiceView.GetCustomer += null;
        LastCall.IgnoreArguments();
        _mockInvoiceView.AddInvoiceLine += null;
        LastCall.IgnoreArguments();
        _mockInvoiceView.CalculateTotals += null;
        var calculateTotalsEventRaiser = LastCall.IgnoreArguments().GetEventRaiser();
        _mockInvoiceView.SaveInvoice += null;
        LastCall.IgnoreArguments();

        ITaxesService taxesService = new TaxesService();
        Expect.Call(_mockTaxesRepository.GetTaxesService()).Return(taxesService);
        var invoice = new Invoice(taxesService);
        _mockInvoiceView.SubTotal = invoice.SubTotal;
        _mockInvoiceView.TaxCalculations = invoice.TaxCalculations;
        _mockInvoiceView.Total = invoice.Total;

        _mockRepository.ReplayAll();

        var invoicePresenter = new InvoicePresenter(_mockCustomerRepository, _mockTaxesRepository, _mockInvoiceRepository, _mockInvoiceView);
        calculateTotalsEventRaiser.Raise(_mockInvoiceView, EventArgs.Empty);
    }

    [Test]
    public void InvoiceSaveInvoiceToRepository()
    {
        _mockInvoiceView.GetCustomer += null;
        LastCall.IgnoreArguments();
        _mockInvoiceView.AddInvoiceLine += null;
        LastCall.IgnoreArguments();
        _mockInvoiceView.CalculateTotals += null;
        LastCall.IgnoreArguments();
        _mockInvoiceView.SaveInvoice += null;
        var saveInvoiceEventRaiser = LastCall.IgnoreArguments().GetEventRaiser();

        ITaxesService taxesService = new TaxesService();
        Expect.Call(_mockTaxesRepository.GetTaxesService()).Return(taxesService);
        var invoice = new Invoice(taxesService);
        _mockInvoiceRepository.SaveInvoice(invoice);

        _mockRepository.ReplayAll();

        var invoicePresenter = new InvoicePresenter(_mockCustomerRepository, _mockTaxesRepository, _mockInvoiceRepository, _mockInvoiceView);
        saveInvoiceEventRaiser.Raise(_mockInvoiceView, EventArgs.Empty);
    }
}


InvoicePresenter.cs

public class InvoicePresenter
{
    private readonly ICustomerRepository _customerRepository;
    private readonly ITaxesRepository _taxesRepository;
    private readonly IInvoiceRepository _invoiceRepository;
    private readonly IInvoiceView _invoiceView;
    private Invoice _invoice;

    public InvoicePresenter(ICustomerRepository customerRepository, ITaxesRepository taxesRepository, 
                                IInvoiceRepository invoiceRepository, IInvoiceView invoiceView)
    {
        _customerRepository = customerRepository;
        _taxesRepository = taxesRepository;
        _invoiceRepository = invoiceRepository;
        _invoiceView = invoiceView;

        _invoiceView.GetCustomer += new System.EventHandler(InvoiceViewGetCustomer);
        _invoiceView.AddInvoiceLine += new System.EventHandler(InvoiceViewAddInvoiceLine);
        _invoiceView.CalculateTotals += new System.EventHandler(InvoiceViewCalculateTotals);
        _invoiceView.SaveInvoice += new System.EventHandler(InvoiceViewSaveInvoice);
    }

    void InvoiceViewSaveInvoice(object sender, System.EventArgs e)
    {
        var invoice = GetInvoice();
        _invoiceRepository.SaveInvoice(invoice);
    }

    void InvoiceViewCalculateTotals(object sender, System.EventArgs e)
    {
        var invoice = GetInvoice();
        _invoiceView.SubTotal = invoice.SubTotal;
        _invoiceView.TaxCalculations = invoice.TaxCalculations;
        _invoiceView.Total = invoice.Total;
    }

    void InvoiceViewAddInvoiceLine(object sender, System.EventArgs e)
    {
        var invoice = GetInvoice();
        invoice.AddLineItem(new InvoiceItem(_invoiceView.Quantity, _invoiceView.Amount));
        _invoiceView.InvoiceLineItems = invoice.InvoiceItems;
    }

    private Invoice GetInvoice()
    {
        if (_invoice == null)
        {
            _invoice = new Invoice(_taxesRepository.GetTaxesService());
        }
        return _invoice;
    }

    void InvoiceViewGetCustomer(object sender, System.EventArgs e)
    {
        var customer = _customerRepository.FindCustomerByCode(_invoiceView.CustomerCode);
        _invoiceView.CustomerFirstName = customer.FirstName;
        _invoiceView.CustomerLastName = customer.LastName;
    }
}

Sunday, February 07, 2010

Day 4 of 5 Day TDD Kata: "AddTaxesPresenter Displays Tax Grid and Adds New Taxes"

I've just completed Day 4 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 4: AddTaxesPresenter DISPLAYS TAX GRID AND ADDS NEW TAXES
  1. Create namespaces for Presenter and View.
  2. Create TaxesPresenterTests with mocked ITaxesService and ITaxesView.
  3. Create ITaxesView.ShowTaxes event with default EventHandler. Verify that TaxesPresenter attaches an event handler to ShowTaxes in its constructor.
  4. Verify that the view's ShowTaxes event, when raised, is handled by TaxesPresenter's event handler, which should assign ITaxesService.Taxes to ITaxesView.TaxesDisplay.
  5. Create ITaxesView.AddTax event with default EventHandler. Update your first and second tests to verify that TaxPresenter's constructor now also assigns the event handler for AddTaxEvent.
  6. Verify that when AddTax event is raised, it is handled by TaxesPresenter's event handler as follows:
    a) Each required Tax constructor parameter exists as a get property of ITaxesView, and each of those properties is called (returning a value specified in the mock).
    b) it creates Tax instance with the property values assigned to the mock.

    c) it calls ITaxesService.AddTax() to add the Tax.
    d) it refreshes ITaxesView.TaxesDisplay from ITaxesService.Taxes, which includes the just-added Tax instance.
TaxesPresenterTests.cs

[TestFixture]
public class TaxesPresenterTests
{
    private MockRepository _mockRepository;
    private ITaxesView _mockTaxesView;
    private ITaxesService _mockTaxesService;

    [SetUp]
    public void SetUp()
    {
        _mockRepository = new MockRepository();
        _mockTaxesService = _mockRepository.StrictMock();
        _mockTaxesView = _mockRepository.StrictMock();
    }

    [TearDown]
    public void TearDown()
    {
        _mockRepository.VerifyAll();
    }

    [Test]
    public void TaxesPresenterAttachesViewEventHandlersOnConstruction()
    {
        _mockTaxesView.ShowTaxes += null;
        LastCall.IgnoreArguments();
        _mockTaxesView.AddTax += null;
        LastCall.IgnoreArguments();

        _mockRepository.ReplayAll();

        var taxesPresenter = new TaxesPresenter(_mockTaxesService, _mockTaxesView);
    }

    [Test]
    public void ShowTaxesEventDisplayAllTaxes()
    {
        _mockTaxesView.ShowTaxes += null;
        var showTaxesEventRaiser = LastCall.IgnoreArguments().GetEventRaiser();
        _mockTaxesView.AddTax += null;
        LastCall.IgnoreArguments();
        var taxes = new List();
        Expect.Call(_mockTaxesService.Taxes).Return(taxes);
        _mockTaxesView.TaxesDisplay = taxes;

        _mockRepository.ReplayAll();

        var taxesPresenter = new TaxesPresenter(_mockTaxesService, _mockTaxesView);
        showTaxesEventRaiser.Raise(_mockTaxesView, EventArgs.Empty);
    }

    [Test]
    public void AddTaxEventCallsITaxesServiceAddAndReassignsToGridWithExtraRow()
    {
        _mockTaxesView.ShowTaxes += null;
        LastCall.IgnoreArguments();
        _mockTaxesView.AddTax += null;
        var addTaxEventRaiser = LastCall.IgnoreArguments().GetEventRaiser();
        const string taxType = "pstTax";
        DateTime? startDate = DateTime.Today;
        DateTime? endDate = DateTime.Today.AddYears(1);
        const JurisdictionEnum jurisdiction = JurisdictionEnum.ProvinceState;
        const int percent = 5;
        Expect.Call(_mockTaxesView.TaxType).Return(taxType);
        Expect.Call(_mockTaxesView.StartDate).Return(startDate);
        Expect.Call(_mockTaxesView.EndDate).Return(endDate);
        Expect.Call(_mockTaxesView.Jurisdiction).Return(jurisdiction);
        Expect.Call(_mockTaxesView.Percent).Return(percent);
        var tax = new Tax(taxType, startDate, endDate, jurisdiction, percent);
        _mockTaxesService.AddTax(tax);
        var taxes = new List { tax };
        Expect.Call(_mockTaxesService.Taxes).Return(taxes);
        _mockTaxesView.TaxesDisplay = taxes;

        _mockRepository.ReplayAll();

        var taxesPresenter = new TaxesPresenter(_mockTaxesService, _mockTaxesView);
        addTaxEventRaiser.Raise(_mockTaxesView, EventArgs.Empty);
    }
}


TaxesPresenter.cs


public class TaxesPresenter
{
    private readonly ITaxesService _taxesService;
    private readonly ITaxesView _taxesView;

    public TaxesPresenter(ITaxesService taxesService, ITaxesView taxesView)
    {
        _taxesService = taxesService;
        _taxesView = taxesView;

        _taxesView.ShowTaxes += new System.EventHandler(TaxesViewShowTaxes);
        _taxesView.AddTax += new System.EventHandler(TaxesViewAddTax);
    }

    void TaxesViewAddTax(object sender, System.EventArgs e)
    {
        var tax = new Tax(_taxesView.TaxType, _taxesView.StartDate, _taxesView.EndDate, _taxesView.Jurisdiction, _taxesView.Percent);
        _taxesService.AddTax(tax);
        DisplayAllTaxes();
    }

    void TaxesViewShowTaxes(object sender, System.EventArgs e)
    {
        DisplayAllTaxes();
    }

    private void DisplayAllTaxes()
    {
        _taxesView.TaxesDisplay = _taxesService.Taxes;
    }
}

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;
    }

}

Friday, February 05, 2010

Day 2 of 5 Day TDD Kata: "City Tax Validation is Teased Out into new ITaxesService"

I've just completed Day 2 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 2: TEASING OUT DOMAIN OBJECT: ITaxesService
  1. Create JurisdictionEnum { City, State, Country }
  2. Alter TaxesTest test methods to now test for Jurisdiction as required 4th Tax property. 
  3. Add new constructor parameter to City: ITaxesService, so that City constructor calls in CityTests will no longer compile.
  4. Comment out ALL public and private methods in City, so that the Add(Tax tax) method call in CityTests will no longer compile.
  5. Since the tests no longer compile, comment out all test methods in the CityTests class.
  6. Reference a mocking framework and add a using statement to CityTests class.
  7. Create new test in CityTests which verifies that when City is asked to AddTax(), that it delegates tax adding to the mocked ITaxesService. City should be agnostic to tax storage.
  8. Create new test in ProvinceTests which verifies that when Province is asked to AddTax(), that it delegates tax adding to the mocked ITaxesService. Province should be agnostic to tax storage.
  9. Create new test in CountryTests which verifies that when Country is asked to AddTax(), that it delegates tax adding to the mocked ITaxesService. Country should be agnostic to tax storage.
  10. Create a TaxesServiceTests class.
  11. Move the commented-out CityTest methods to TaxesServiceTests class and repurpose them to address the TaxesService.Add() method and Taxes property. 
  12. Notes: When creating TaxesService, make it implement ITaxesService, and use a parameterless constructor. Remember to move the commented out public and private methods from City class to the new TaxesService class--you'll need them for the tests to pass.
  13. Validate that tax duplication checking logic now constrains on BOTH TaxType AND Jurisdiction.
  14. For extra points: Create a test which instantiates City, Province, and Country, injecting each with a common stub of ITaxesService and adding a tax from each jurisdiction. Validate that 3 taxes have been added to ITaxesService stub. Also validate that each class' Taxes collection returns only taxes for that jurisdiction.


CityTests.cs

[TestFixture]
public class CityTests
{
    private MockRepository _mockRepository;
    private ITaxesService _mockTaxesService;

    [SetUp]
    public void SetUp()
    {
        _mockRepository = new MockRepository();
        _mockTaxesService = _mockRepository.StrictMock();
    }

    [TearDown]
    public void TearDown()
    {
        _mockRepository.ReplayAll();
        _mockRepository.VerifyAll();
    }

    [Test]
    public void CityDelegatesAddedTaxesToInjectedTaxesService()
    {
        // expectations
        var pstTax = new Tax("PST", DateTime.Today, DateTime.Today.AddMonths(6), JurisdictionEnum.City);
        _mockTaxesService.AddTax(pstTax);

        _mockRepository.ReplayAll();

        var city = new City("Winnipeg", _mockTaxesService);
        city.AddTax(pstTax);            
    }
}


TaxesServiceTests.cs

[TestFixture]
public class TaxesServiceTests
{
    [Test]
    public void TaxesServiceCanAccumulateTaxes()
    {
        var taxesService = new TaxesService();
        var pstTax = new Tax("PST", DateTime.Today, DateTime.Today.AddMonths(6), JurisdictionEnum.City);
        taxesService.AddTax(pstTax);

        Assert.IsTrue(taxesService.Taxes.Contains(pstTax));
    }

    [Test]
    [ExpectedException(typeof(DuplicateTaxesException))]
    public void TaxesServiceRejectsDuplicateTaxes()
    {
        var taxesService = new TaxesService();
        var pstTax1 = new Tax("PST", DateTime.Today, DateTime.Today.AddMonths(6), JurisdictionEnum.City);
        var pstTax2 = new Tax("PST", DateTime.Today, DateTime.Today.AddMonths(6), JurisdictionEnum.City);
        taxesService.AddTax(pstTax1);
        taxesService.AddTax(pstTax2);
    }

    [Test]
    [ExpectedException(typeof(OverlappingTaxTypesException))]
    public void TaxesServiceRejectsOverlappingTaxesPerTaxType()
    {
        var taxesService = new TaxesService();
        var pstTax1 = new Tax("PST", DateTime.Today, DateTime.Today.AddMonths(6), JurisdictionEnum.City);
        var pstTax2 = new Tax("PST", DateTime.Today.AddMonths(6), DateTime.Today.AddYears(1), JurisdictionEnum.City);
        taxesService.AddTax(pstTax1);
        taxesService.AddTax(pstTax2);
    }
}

Thursday, February 04, 2010

Day 1 of 5 Day TDD Kata: "Taxes Are Validated As They Are Added to City"

I've just completed Day 1 of the 5 Day TDD kata in about 40 minutes:

Samples posted to github here.

DAY 1: TAXES ARE VALIDATED AS THEY ARE ADDED TO CITY
  1. Tax must be created with its 3 properties: TaxType, StartDate, and EndDate, none of which can be null.
  2. StartDate must be less than EndDate
  3. Equality is based on the 3 properties together
  4. City has Taxes.
  5. City rejects duplicate Taxes (by object equality.)
  6. City rejects overlapping taxes (EndDate > other tax start date) for a given TaxType.
To achieve this:

I first created tests for Tax, which validated that it was fed all properties from the constructor, did not allow nulls, based object equality on all 3 properties, and did not allow EndDate earlier than StartDate.

I then created tests for City, which added tax objects to Taxes collection, rejected duplicates, and rejected overlapping EndDates with StartDate by tax type.

Tests and classes below.

TaxTests.cs

[TestFixture]
public class TaxTests
{
    [Test]
    [ExpectedException(typeof(TaxValuesMissingException))]
    public void TaxCannotBeCreatedWithAllNullProperties()
    {
        var tax = new Tax(null, null, null);
    }

    [Test]
    [ExpectedException(typeof(TaxValuesMissingException))]
    public void TaxCannotBeCreatedWithNullTaxType()
    {
        var tax = new Tax(null, DateTime.Today.AddDays(1), DateTime.Today.AddYears(1));
    }

    [Test]
    [ExpectedException(typeof(TaxValuesMissingException))]
    public void TaxCannotBeCreatedWithNullStartDate()
    {
        var tax = new Tax("PST", null, DateTime.Today.AddYears(1));
    }

    [Test]
    [ExpectedException(typeof(TaxValuesMissingException))]
    public void TaxCannotBeCreatedWithNullEndDate()
    {
        var tax = new Tax("PST", DateTime.Today.AddDays(1), null);
    }

    [Test]
    public void TaxCanBeCreatedWhenAllPropertiesSupplied()
    {
        var tax = new Tax("PST", DateTime.Today.AddDays(1), DateTime.Today.AddYears(1));
        Assert.IsNotNull(tax);
    }

    [Test]
    [ExpectedException(typeof(InvalidTaxDateRangeException))]
    public void TaxStartDateCannotBeGreaterThanEndDate()
    {
        var tax = new Tax("PST", DateTime.Today.AddYears(1).AddDays(1), DateTime.Today.AddYears(1));
        Assert.IsNotNull(tax);
    }

    [Test]
    public void TaxesAreEqualWhenConstructorParametersMatch()
    {
        var tax1 = new Tax("PST", DateTime.Today.AddDays(1), DateTime.Today.AddYears(1));
        var tax2 = new Tax("PST", DateTime.Today.AddDays(1), DateTime.Today.AddYears(1));

        Assert.IsTrue(tax1.Equals(tax2));
        // Assert.AreSame(tax1, tax2) failed, may be referencing another nunit namespace??
    }
}


Tax.cs

public class Tax
{
    private readonly string _taxType;
    private readonly DateTime? _startDate;
    private readonly DateTime? _endDate;

    private Tax()
    {
    }

    public Tax(string taxType, DateTime? startDate, DateTime? endDate)
    {
        _taxType = taxType;
        _startDate = startDate;
        _endDate = endDate;

        ValidateAllParametersHaveNonNullValue();

        ValidateEndDateGreaterThanStartDate();
    }

    public string TaxType
    {
        get { return _taxType; }
    }

    public DateTime? StartDate
    {
        get { return _startDate; }
    }

    public DateTime? EndDate
    {
        get { return _endDate; }
    }

    public override bool Equals(object obj)
    {
        var otherTax = (Tax) obj;

        var isEqual = otherTax.TaxType.Equals(this.TaxType)
               && otherTax.StartDate.Value.Equals(this.StartDate.Value)
               && otherTax.EndDate.Value.Equals(this.EndDate.Value);

        return isEqual;
    }

    public override int GetHashCode()
    {
        return StartDate.GetHashCode() + EndDate.GetHashCode() + TaxType.GetHashCode();
    }

    private void ValidateEndDateGreaterThanStartDate()
    {
        if (StartDate.Value > EndDate.Value)
            throw new InvalidTaxDateRangeException();
    }

    private void ValidateAllParametersHaveNonNullValue()
    {
        if (TaxType == null
            || !StartDate.HasValue
            || !EndDate.HasValue)
            throw new TaxValuesMissingException();
    }
}


CityTests.cs

[TestFixture]
public class CityTests
{
    [Test]
    public void CityCanAddTaxes()
    {
        var city = new City("Winnipeg");
        var pstTax = new Tax("PST", DateTime.Today, DateTime.Today.AddMonths(6));
        city.AddTax(pstTax);

        Assert.IsTrue(city.Taxes.Contains(pstTax));
    }

    [Test]
    [ExpectedException(typeof(DuplicateTaxesException))]
    public void CityRejectsDuplicateTaxes()
    {
        var city = new City("Winnipeg");
        var pstTax1 = new Tax("PST", DateTime.Today, DateTime.Today.AddMonths(6));
        var pstTax2 = new Tax("PST", DateTime.Today, DateTime.Today.AddMonths(6));
        city.AddTax(pstTax1);
        city.AddTax(pstTax2);
    }

    [Test]
    [ExpectedException(typeof(OverlappingTaxTypesException))]
    public void CityRejectsOverlappingTaxesPerTaxType()
    {
        var city = new City("Winnipeg");
        var pstTax1 = new Tax("PST", DateTime.Today, DateTime.Today.AddMonths(6));
        var pstTax2 = new Tax("PST", DateTime.Today.AddMonths(6), DateTime.Today.AddYears(1));
        city.AddTax(pstTax1);
        city.AddTax(pstTax2);
    }

}


City.cs

public class City
{
    private readonly string _name;

    public City(string name)
    {
        _name = name;
        Taxes = new List();
    }

    public List Taxes { get; private set; }

    public void AddTax(Tax tax)
    {
        if (tax == null) throw new ArgumentNullException("tax");
        RejectDuplicateTaxes(tax);
        RejectOverlappingTaxes(tax);

        this.Taxes.Add(tax);
    }

    private void RejectOverlappingTaxes(Tax tax)
    {
        foreach(var currowTax in this.Taxes)
        {
            if(IsFutureTax(tax, currowTax)
               && FutureTaxOverlapsEndDateOfCurrowTax(tax, currowTax))
                throw new OverlappingTaxTypesException();

            if(IsEarlierTax(tax, currowTax)
                && CurrowTaxOverlapsEndDateOfPreviousTax(tax, currowTax))
                throw new OverlappingTaxTypesException();
        }
    }

    private static bool CurrowTaxOverlapsEndDateOfPreviousTax(Tax tax, Tax currowTax)
    {
        return currowTax.TaxType.Equals(tax.TaxType)
                && currowTax.StartDate <= tax.EndDate;     }     private static bool IsEarlierTax(Tax tax, Tax currowTax)     {         return currowTax.TaxType.Equals(tax.TaxType)                 && tax.StartDate.Value < currowTax.StartDate;     }     private static bool FutureTaxOverlapsEndDateOfCurrowTax(Tax tax, Tax currowTax)     {         return currowTax.TaxType.Equals(tax.TaxType)                 && tax.StartDate <= currowTax.EndDate;     }     private static bool IsFutureTax(Tax tax, Tax currowTax)     {         return currowTax.TaxType.Equals(tax.TaxType)                 && tax.StartDate.Value > currowTax.StartDate;
    }

    private void RejectDuplicateTaxes(Tax tax)
    {
        if(this.Taxes.Contains(tax))
            throw new DuplicateTaxesException();
    }
}

Wednesday, February 03, 2010

5 Day TDD Kata

From Single Class, to Teasing Out Domain, to Model-View-Presenter with Mocks

Samples are posted to github here.

While the Calculator kata has been tremendously beneficial, I am wanting to experiment with a larger TDD kata that attempts test coverage at 3 levels:
  1. For a single class.
  2. For teasing out 1 or more objects in the domain.
  3. For testing presenter with mocks, in Model-View-Presenter, using the newly-created domain.
Below is the initial outline. Over the next 5 days I will do the kata, and adjust the instructions for each day as the design becomes clearer.

Summary
DAY 1: TAXES ARE VALIDATED AS THEY ARE ADDED TO CITY
DAY 2: TEASING OUT DOMAIN OBJECT: ITaxesService
DAY 3: INVOICE APPLIES TAXES TO ITEMS VIA ITaxesService
DAY 4: AddTaxesPresenter DISPLAYS TAX GRID AND ADDS NEW TAXES
DAY 5: InvoicePresenter ADDS AND DISPLAYS INVOICE WITH TAXES AND TOTALS

Detail
DAY 1: TAXES ARE VALIDATED AS THEY ARE ADDED TO CITY
  1. Tax must be created with its 3 properties: TaxType, StartDate, and EndDate, none of which can be null.
  2. StartDate must be less than EndDate
  3. Equality is based on the 3 properties together
  4. City has Taxes.
  5. City rejects duplicate Taxes (by object equality.)
  6. City rejects overlapping taxes (EndDate > other tax start date) for a given TaxType.

DAY 2: TEASING OUT DOMAIN OBJECT: ITaxesService
  1. Create JurisdictionEnum { City, State, Country }
  2. Alter TaxesTest test methods to now test for Jurisdiction as required 4th Tax property. 
  3. Add new constructor parameter to City: ITaxesService, so that City constructor calls in CityTests will no longer compile.
  4. Comment out ALL public and private methods in City, so that the Add(Tax tax) method call in CityTests will no longer compile.
  5. Since the tests no longer compile, comment out all test methods in the CityTests class.
  6. Reference a mocking framework and add a using statement to CityTests class.
  7. Create new test in CityTests which verifies that when City is asked to AddTax(), that it delegates tax adding to the mocked ITaxesService. City should be agnostic to tax storage.
  8. Create new test in ProvinceTests which verifies that when Province is asked to AddTax(), that it delegates tax adding to the mocked ITaxesService. Province should be agnostic to tax storage.
  9. Create new test in CountryTests which verifies that when Country is asked to AddTax(), that it delegates tax adding to the mocked ITaxesService. Country should be agnostic to tax storage.
  10. Create a TaxesServiceTests class.
  11. Move the commented-out CityTest methods to TaxesServiceTests class and repurpose them to address the TaxesService.Add() method and Taxes property. 
  12. Notes: When creating TaxesService, make it implement ITaxesService, and use a parameterless constructor. Remember to move the commented out public and private methods from City class to the new TaxesService class--you'll need them for the tests to pass.
  13. Validate that tax duplication checking logic now constrains on BOTH TaxType AND Jurisdiction.
  14. For extra points: Create a test which instantiates City, Province, and Country, injecting each with a common stub of ITaxesService and adding a tax from each jurisdiction. Validate that 3 taxes have been added to ITaxesService stub. Also validate that each class' Taxes collection returns only taxes for that jurisdiction.

    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).

    DAY 4: AddTaxesPresenter DISPLAYS TAX GRID AND ADDS NEW TAXES

    1. Create namespaces for Presenter and View.
    2. Create TaxesPresenterTests with mocked ITaxesService and ITaxesView.
    3. Create ITaxesView.ShowTaxes event with default EventHandler. Verify that TaxesPresenter attaches an event handler to ShowTaxes in its constructor.
    4. Verify that the view's ShowTaxes event, when raised, is handled by TaxesPresenter's event handler, which should assign ITaxesService.Taxes to ITaxesView.TaxesDisplay.
    5. Create ITaxesView.AddTax event with default EventHandler. Update your first and second tests to verify that TaxPresenter's constructor now also assigns the event handler for AddTaxEvent.
    6. Verify that when AddTax event is raised, it is handled by TaxesPresenter's event handler as follows:
      a) Each required Tax constructor parameter exists as a get property of ITaxesView, and each of those properties is called (returning a value specified in the mock).
      b) it creates Tax instance with the property values assigned to the mock.

      c) it calls ITaxesService.AddTax() to add the Tax.
      d) it refreshes ITaxesView.TaxesDisplay from ITaxesService.Taxes, which includes the just-added Tax instance.
    DAY 5: InvoicePresenter ADDS AND DISPLAYS INVOICE WITH TAXES AND TOTALS

          The culmination. This kata works with everything you have built leading up to it.
    1. Create a namespace for Repository.
    2. Create CustomerTests. Verify when Customer adds an Invoice, that Customer.Invoices increments.
    3. Create InvoicePresenterTests with 4 mocked interfaces: ITaxesRepository, ICustomerRepository, IInvoiceRepository, and IInvoiceView.
    4. Create the following IInvoiceView events, each with a default EventHandler:

      • GetCustomer event
      • AddInvoiceLine event
      • CalculateTotals event
      • SaveInvoice event 
    5. For each one, verify that InvoicePresenter's constructor attaches that event to an event handler. (You can expect each event to be assigned to null, and instruct the mock to ignore the argument. For subsequent tests, you only need to get the event if the current test needs to verify behaviour that occurs when the event is raised.)
    6. When GetCustomer event is raised, verify that
      a) IInvoiceView.CustomerCode is assigned to ICustomerRepository.FindCustomerByCode().
      b) the returned Customer is retrieved, and FirstName and LastName are assigned to IInvoiceView.FirstName and IInvoiceView.LastName. 
    7. When AddInvoiceLine event is raised, verify that:
      a) IInvoiceView.Quantity and IInvoiceView.Amount are retrieved and assigned to InvoiceItem constructor.
      b) ITaxesRepository.GetTaxesService() is called and returns ITaxesService.
      c) Invoice is instantiated with ITaxesService, and InvoiceItem is added to Invoice.AddLineItem()
      d) Invoice.LineItems is assigned to IInvoiceView.InvoiceLineItems property.
      e) When the last expectation fails, add equality checking (IsEqual() and GetHashCode()) to InvoiceItem class, based on properties of InvoiceItem (quantity and amount.)
    8. When CalculateTotals event is raised, verify that:
      a) Invoice.SubTotal is assigned to IInvoiceView.Subtotal.
      b) Invoice.TaxCalculations are assigned to IInvoiceView.TaxCalculations.
      c) Invoice.Total is assigned to IInvoiceView.Total. 
    9. When SaveInvoice event is raised, verify that:
      a) IInvoiceRepository.SaveInvoice() is passed the current Invoice instance.
      b) When this expecation fails, add equality checking (IsEqual() and GetHashCode() to Invoice, based on 0 items equal, or equality of items.
    Congratulations! You have now completed the 5 Day TDD kata!

    A reminder: samples are posted to github here.