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.

    No comments: