Thursday, January 21, 2010

TDD/mocks kata for MVP (Model-View-Presenter)

Here is a simple TDD Kata for Model-View-Presenter and Rhino Mocks.

Model-View-Presenter is an implementation approach rather than a framework. Therefore it can be used to create testable sub-presentation layers for any GUI platform (WinForm .NET, ASP.NET, Java Swing, SharePoint web controls, you name it) because the view is abstracted to an interface. All of the (testable) interaction logic now occurs in a newly created layer called the presentation layer. The presentation layer is completely agnostic about the view implementation; in fact, it can have MULTIPLE view implementations.

Finally, any time that an SUT (a "system-under-test") is created which interacts with other code through interfaces, those interfaces can (and usually should) be tested with unit tests that isolate the SUT. This is done by either faking the interface implementations (with minimal "fake" implementation classes) or mocking the implementations with mocking tools such as jMock (for Java), or NMock, Moq, or RhinoMocks for .NET.

In this TDD kata example, RhinoMocks is used as the mocking framework.

Model-View-Presenter TDD Kata
PreRequisites:
Create solution.
Reference TDD framework and mocking framework.
Create namespaces for Model, View, Presenter, and UnitTests.

NOTE  For each step which follows, code samples are shown with possible implementations (below).

1) Create a presenter class which instantiates two interfaces: mock repository and view. Use naming prefix "Customer" on the presenter, the repository, and the view.
2) In the View, create an event: Initialize and a  string property: PageTitle
3) Verify that when the Initialize event is raised:
   * the view's PageTitle property is set to "Welcome".
4) Create a Customer in the Model with properties FirstName and LastName.
5) In the View, create an event: GetCustomers and a List property: Customers.
6) Verify that when the GetCustomers event is raised:
   * the repository method GetCustomers()  is called and returns a list of Customers
   * the view's Customers property is set to the Customers list
7) Create a SortCustomerEventHandler delegate with SortCustomerEventArgs that passes SortExpression and IsAscending.
8) In the View, create an event: SortCustomer (using the SortCustomerEventHandler delegate)
9) Verify that when the SortCustomer event is raised:
   * SortExpression and SortDirection properties are passed to SortCustomerEventArgs
   * [possibly that the sort has occured]
   * the view's Customers property is set to the Customers list

Here is what one possible test output look like using RhinoMocks:

[TestFixture]
public class CustomerPresenterTests
{
    private readonly MockRepository _mockRepository = new MockRepository();
    private CustomerPresenter _customerPresenter;
    private ICustomerRepository _mockCustomerRepository;
    private ICustomerView _mockCustomerView;

    [SetUp]
    public void Setup()
    {
        _mockCustomerView = _mockRepository.StrictMock();
        _mockCustomerRepository = _mockRepository.StrictMock();
    }

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

        _mockRepository.VerifyAll();
    }

    [Test]
    public void CustomerPresenter_Can_Be_Instantiated()
    {
        _mockCustomerView.Initialize += null;
        LastCall.IgnoreArguments();
        _mockCustomerView.GetCustomers += null;
        LastCall.IgnoreArguments();
        _mockCustomerView.SortCustomers += null;
        LastCall.IgnoreArguments();

        _mockRepository.ReplayAll();

        _customerPresenter = new CustomerPresenter(_mockCustomerRepository, _mockCustomerView);
    }

    [Test]
    public void CustomerPresenter_Sets_ViewTitle_When_Initialize_Event_Raised()
    {
        _mockCustomerView.Initialize += null;
        var initializeEventRaised = LastCall.IgnoreArguments().GetEventRaiser();
        _mockCustomerView.GetCustomers += null;
        LastCall.IgnoreArguments();
        _mockCustomerView.SortCustomers += null;
        LastCall.IgnoreArguments();
        _mockCustomerView.PageTitle = "Welcome";

        _mockRepository.ReplayAll();

        _customerPresenter = new CustomerPresenter(_mockCustomerRepository, _mockCustomerView);
        initializeEventRaised.Raise(_mockCustomerView, EventArgs.Empty);
    }

    [Test]
    public void CustomerPresenter_GetsCustomers_When_GetCustomers_Event_Raised()
    {
        var customers = new List();

        _mockCustomerView.Initialize += null;
        LastCall.IgnoreArguments();
        _mockCustomerView.GetCustomers += null;
        var getCustomersEventRaised = LastCall.IgnoreArguments().GetEventRaiser();
        _mockCustomerView.SortCustomers += null;
        LastCall.IgnoreArguments();
        Expect.Call(_mockCustomerRepository.GetCustomers()).Return(customers);
        _mockCustomerView.Customers = customers;

        _mockRepository.ReplayAll();

        _customerPresenter = new CustomerPresenter(_mockCustomerRepository, _mockCustomerView);
        getCustomersEventRaised.Raise(_mockCustomerView, EventArgs.Empty);
    }

    [Test]
    public void CustomerPresenter_SortsCustomers_When_SortCustomers_Event_Raised()
    {
        var customers = new List();

        _mockCustomerView.Initialize += null;
        LastCall.IgnoreArguments();
        _mockCustomerView.GetCustomers += null;
        LastCall.IgnoreArguments();
        _mockCustomerView.SortCustomers += null;
        var getSortCustomerEventRaiser = LastCall.IgnoreArguments().GetEventRaiser();
        Expect.Call(_mockCustomerRepository.GetCustomers()).Return(customers);
        _mockCustomerView.Customers = customers;

        _mockRepository.ReplayAll();

        _customerPresenter = new CustomerPresenter(_mockCustomerRepository, _mockCustomerView);
        var sce = new SortCustomersEventArgs("", true);
        getSortCustomerEventRaiser.Raise(_mockCustomerView, sce);
    }
}



And here is what one possible CustomerPresenter implementation looks like:

public class CustomerPresenter
{
    private readonly ICustomerRepository _customerRepository;
    private readonly ICustomerView _customerView;

    public CustomerPresenter(ICustomerRepository customerRepository, ICustomerView customerView)
    {
        _customerRepository = customerRepository;
        _customerView = customerView;

        _customerView.Initialize += CustomerViewInitialize;
        _customerView.GetCustomers += CustomerViewGetCustomers;
        _customerView.SortCustomers += CustomerViewSortCustomers;
    }

    void CustomerViewSortCustomers(object sender, SortCustomersEventArgs sce)
    {
        List customers = _customerRepository.GetCustomers();

        customers.Sort(delegate(Customer first, Customer second)
        {
            int result;

            switch (sce.SortExpression)
            {
                case "FirstName":
                    result = first.FirstName.CompareTo(second.FirstName);
                    break;
                case "LastName":
                    result = first.LastName.CompareTo(second.LastName);
                    break;
                default:
                    result = 0;
                    break;
            }

            return (sce.IsAscending) ? result : -result;
        });

        _customerView.Customers = customers;
    }

    private void CustomerViewGetCustomers(object sender, EventArgs e)
    {
        List customers = _customerRepository.GetCustomers();
        _customerView.Customers = customers;
    }

    private void CustomerViewInitialize(object sender, EventArgs e)
    {
        _customerView.PageTitle = "Welcome";
    }
}



I've tried a couple of implementations of ICustomerView: WinForm, and as a Sharepoint web part. Interesting to see it work.

But, from the kata point of view, the only part that matters is repeating the creation of the tests, and experiementing/refining the approach.

No comments: