Balancing Act

Unit Testing Presentation Logic in ASP.NET 2.0

CoverStory

LANGUAGES: C# | VB.NET

ASP.NET VERSIONS: 2.0

 

Balancing Act

Unit Testing Presentation Logic in ASP.NET 2.0

 

By Nicholas Roeder

 

Although it is easy and tempting to write all application code in the ASP.NET code-behind, it is commonly known as a bad design practice. Not only does this design lead to large amounts of duplicate code, it is difficult to unit test in an automated fashion. The typical strategy is to use a layered architecture that moves code into other layers that are easily testable.

 

However, even after applying a layered approach we are still left with difficult-to-test code in the code-behind, such as responding to UI events and orchestrating presentation logic. When creating ASP.NET Web sites in Visual Studio 2005, projects cannot reference a Web site project, so writing unit tests in another assembly for Web forms is impossible.

 

One solution is to write page-level tests using a tool like NUnitAsp. The disadvantages of this approach include slower test execution and more fragile tests based on UI controls. This type of functional testing also does not meet the goal of testing in isolation at the unit level.

 

Another solution is to apply the Model View Presenter (MVP) design pattern (see http://msdn.microsoft.com/msdnmag/issues/06/08/DesignPatterns/default.aspx), writing unit-testable logic in the presenter class. However, a pure approach to applying this pattern will lead to writing tedious manual data binding code.

 

This article explores an approach to writing unit-testable presentation logic using a variant of the MVP design pattern and mock objects, taking advantage of two-way data binding using the ObjectDataSource in ASP.NET 2.0.

 

Supervising Presenter

Many design patterns deal with separating presentation logic from the user interface. A variant of Model View Presenter, dubbed the Supervising Presenter (or Controller) pattern by Martin Fowler (http://www.martinfowler.com), takes advantage of the platform s data binding functionality. Applying this pattern reduces the unit testing coverage for data binding code, but gains the power of declarative data binding. Written manually, data binding code can significantly bloat a solution.

 

The basic structure of this pattern as used in an ASP.NET environment is shown in Figure 1. Each Web form implements a view interface. The Web form delegates event handling and presentation logic to the presenter, which is created on page initialization. The page passes itself as the view to the constructor of the presenter. The main point is to keep the view interface (and therefore the code-behind) as simple and passive as possible and move as much logic into the presenter. Depending on the requirements, the presenter may need to communicate with various model objects to carry out the task.

 


Figure 1: A Supervising Presenter class diagram.

 

The goal is to be able to write presenter unit tests. To do so, we can test its interaction with the view and model objects.

 

An Example

An example will illustrate this approach to unit testing presentation logic. The page shown in Figure 2 will be used. The page s purpose is to allow the user to maintain an inventory of books. The GridView shows the complete list of books. Selecting an individual book in the list displays the book information in an editable DetailsView.

 


Figure 2: A sample Web form.

 

The Visual Studio solution for this example is shown in Figure 3. Besides a Web site, we have two class libraries: one for application logic (presentation, business, etc.) and one for unit tests. Typically it s cleaner to avoid deploying unit tests, so they are placed in their own assembly. The test assembly can reference the application assembly, but cannot refer to the Web site (a limitation of using Web site projects in Visual Studio 2005). In a real application we d likely also have a data access layer and a database; however, the details of the data access layer, and unit testing of the data access and business classes, are out of the scope of this discussion.

 


Figure 3: A Visual Studio 2005 solution.

 

Implementing the Web Form

The Web form uses multiple ObjectDataSource controls to handle the data binding. To keep the example simple, the data source is bound to a Book business object, which will simply hold the data in a static collection. The Book class also has properties like Title, Price, and Year. (See the full code in the accompanying download; see end of article for download details.) The DetailsView can bind to an ObjectDataSource dependent on what is selected in the GridView.

 

We can begin creating the code-behind by making it implement the IBooksView interface and instantiate a BooksPresenter, passing the page into the constructor as the view:

 

public partial class Books : Page, IBooksView

{

 private BooksPresenter presenter;

 protected override void OnInit(EventArgs e)

 {

   base.OnInit(e);

   presenter = new BooksPresenter(this);

 }

}

 

The next step is to create the presenter class and view interface in a class library project, separate from the Web site, making sure the Web site references this project. The presenter will likely need the Book class (the model), so we ll add that, too. In a larger application the presenter would typically communicate with the model through a service layer:

 

namespace App.Presentation

{

 public interface IBooksView {}

 public class BooksPresenter

 {

   private IBooksView view;

   private Book book;

   public BooksPresenter(IBooksView view)

     : this(view, new Book())

   {

   }

   public BooksPresenter(IBooksView view, Book book)

   {

     this.view = view;

     this.book = book;

   }

 }

}

 

We now have the structure in place, but the code doesn t do anything. When the user selects a book in the GridView, the DetailsView should change to ReadOnly mode. To handle this, the page hooks up to a method on the presenter the SelectedIndexChanged event on the GridView (gvBooks). The event handler on the presenter can then request the DetailsView control from the view and change its mode.

 

Books (Web form):

 

protected override void OnInit(EventArgs e)

{

 ...

 gvBooks.SelectedIndexChanged +=

   new EventHandler(presenter.SelectedBookChanged);

 ...

 }

}

 

BooksPresenter:

 

public void SelectedBookChanged(object sender, EventArgs e)

{

 DetailsView bookDetails = view.BookDetails;

 bookDetails.ChangeMode(DetailsViewMode.ReadOnly);

}

 

So far we ve let the ObjectDataSource controls take care of all data binding. Assume though, that a business rule states that only unavailable books can be deleted. If the user attempts to delete a book with a different status, an error message should be displayed.

 

We can implement this by handling the Deleted event on the ObjectDataSource (dsBook) itself. As usual, the presenter will implement the event handler. In this case, the event arguments tell us whether an exception was thrown when attempting to delete. The Book class implements the business rule and throws an exception if the rule is broken. Our event handler needs to check the arguments for an exception. If one is thrown, the presenter instructs the view to display an error message and marks the exception as handled. To actually display the error message, the page implements the ErrorMessage property defined on the view and displays the message in a label.

 

Books (Web form):

 

protected override void OnInit(EventArgs e)

{

 ...

 dsBook.Deleted += new

 ObjectDataSourceStatusEventHandler(

 presenter.BookDeleted

 );

 ...

}

 

BooksPresenter:

 

public void BookDeleted(object sender,

 ObjectDataSourceStatusEventArgs e)

{

 if (e.Exception == null || e.Exception.InnerException == null)

     return;

 view.ErrorMessage = e.Exception.InnerException.Message;

 e.ExceptionHandled = true;

}

 

A final example of presentation logic is the case of adding a feature to increase the price of all books by a given percentage. We have a textbox for the number entered and a button to apply the increase. The presenter handles the Click event on the button. In this case, the presenter needs to interact with both the view and the model. The view provides the price adjustment entered; this is passed to a Book class business method that increases prices. Finally, the view is updated by rebinding the GridView and displaying an information message to the user.

 

BooksPresenter:

 

public void AdjustPriceClicked(object sender, EventArgs e)

{

 decimal priceAdjustPercent =

   Convert.ToDecimal(view.PriceAdjustPercent);

 book.AdjustPrice(priceAdjustPercent);

 view.BindBooks();

 view.InfoMessage = string.Format(AdjustPriceMessage,

   priceAdjustPercent);

}

 

Books (Web form):

 

public string PriceAdjustPercent

{

 get { return tbAdjustPrice.Text; }

}

public void BindBooks()

{

 gvBooks.DataBind();

}

public string InfoMessage

{

 set

 {

   lblMessage.Text = value;

   lblMessage.CssClass = "message";

 }

}

 

The sample application implements more presentation logic than described here. See the code download for handling other events like GridView formatting and server-side validation.

 

Unit Testing the Presenter

Now that the form is implemented, what kinds of logic can we test on the presenter? Here is a starting list:

  • UI event handling
  • data source event handling
  • showing/hiding controls
  • special UI formatting logic
  • server-side validation

 

Testing the presenter in isolation requires stand-ins for any dependencies. Mock objects can be used as stand-ins; testing with them involves verifying interaction expectations. Rhino Mocks (http://www.ayende.com/projects/rhino-mocks.aspx) is a dynamic mock object framework for .NET. An important feature is the use of real method calls rather than quoted strings, useful in supporting automated refactoring. The example uses Rhino Mocks in conjunction with NUnit for unit testing.

 

The test will not have access to the code-behind (one of the main reasons we are using the presenter), so we must mock the view interface. A design choice needs to be made whether to mock the model classes or not. The model should be mocked to test the presenter purely in isolation. Some mock object frameworks only support mocking interfaces. This would require additional interfaces be defined, and is not always worth the trouble. Rhino Mocks supports mocking classes with virtual methods. Our presenter has minimal interaction with the model, so to keep the example simple we ll make virtual any method we need to test on the model.

 

When using Rhino Mocks, a mock repository needs to be created before any dynamic mock objects can be instantiated. After each test the VerifyAll method can be called on the repository to ensure that all expectations were met. Because this housekeeping needs to be done for each unit test, the set-up and tear-down methods are good places to implement it. For testing BooksPresenter, we can assume that each test will need to instantiate the presenter, the mock view, and the model in the test set up:

 

namespace Test.Presentation

{

  [TestFixture]

 public class BooksPresenterTest

 {

   private MockRepository mocks;

   private BooksPresenter presenter;

   private IBooksView view;

   private Book book;

    [SetUp]

   public void SetUp()

   {

     mocks = new MockRepository();

     view = mocks.CreateMock<>();

     book = mocks.CreateMock<>();

     presenter = new BooksPresenter(view, book);

   }

    [TearDown]

   public void TearDown()

   {

     mocks.VerifyAll();

   }

 }

}

 

A simple first test is to ensure the SelectedIndexChanged event on the GridView is handled correctly. We expect the view will be asked to return the DetailsView and that, once the event is handled, the DetailsView mode will be ReadOnly. Note that Rhino Mocks requires we indicate when we are finished setting up the expectations by calling ReplayAll on the mock repository. A regular NUnit assertion is used to check the state of the DetailsView after executing the event handler:

 

[Test]

public void ShouldHandleSelectedBookChanged()

{

 // setup expectations

 DetailsView dv = new DetailsView();

 dv.ChangeMode(DetailsViewMode.Insert);

 Expect.Call(view.BookDetails).Return(dv);

 mocks.ReplayAll();

 // execute event handler under test

 presenter.SelectedBookChanged(null, null);

 // assert expected state

 Assert.AreEqual(DetailsViewMode.ReadOnly, dv.CurrentMode);

}

 

Another test is to check whether we correctly handle when a book is deleted. Remember that if a book s status is anything but unavailable, deletion is not allowed. We have at least two cases to test, depending on if the business rule is met or not. The test can call the BookDeleted event handler on the presenter passing in the ObjectDataSourceStatusEventArgs. Even though we are using the declarative data binding functionality, we can test some of the data binding logic. This is because of the comprehensive set of events handled by the ObjectDataSource.

 

In the case where the business rule is met, we don t have any interaction with the view. Note that the ReplayAll method on the mock repository must still be called to indicate we are no longer recording expectations. In this simple case, we assert that the arguments have no exception. The specific exception we are trying to handle is wrapped by the data source, so we actually need to examine the inner exception. The following two tests take care of these scenarios:

 

[Test]

public void ShouldHandleBookDeleted()

{

 mocks.ReplayAll();

 ObjectDataSourceStatusEventArgs args =

   new ObjectDataSourceStatusEventArgs(null, null, null);

 presenter.BookDeleted(null, args);

 Assert.IsNull(args.Exception);

 Assert.IsFalse(args.ExceptionHandled);

}

[Test]

public void ShouldHandleBookDeletedWithNullInnerException()

{

 mocks.ReplayAll();

 ObjectDataSourceStatusEventArgs args =

   new ObjectDataSourceStatusEventArgs(null, null,

     new Exception("test"));

 presenter.BookDeleted(null, args);

 Assert.IsNull(args.Exception.InnerException);

 Assert.IsFalse(args.ExceptionHandled);

}

 

In the case where the business rule is not met, we expect that the view will be told to display an error message. We simulate the scenario by passing event arguments that contain an exception. Finally, the end of the test asserts that the exception-handled flag has been set:

 

[Test]

public void ShouldHandleBookDeletedException()

{

 view.ErrorMessage = Book.DeleteErrorMessage;

 mocks.ReplayAll();

 ObjectDataSourceStatusEventArgs args =

   new ObjectDataSourceStatusEventArgs(null, null,

     new Exception("test",

       new Exception(Book.DeleteErrorMessage)));

 presenter.BookDeleted(null, args);

 Assert.IsTrue(args.ExceptionHandled);

}

 

A last example unit test is handling the function to increase all book prices by a given percentage. With this test, we test the AdjustPriceClicked event handler on the presenter. First we must set up the expectations. The view will be called to query the percentage entered by the user. Then we expect the AdjustPrice method to be called on the Book class. To ensure the AdjustPrice method is mocked we mark it as virtual. The remaining expectations include rebinding the GridView and displaying an information message:

 

[Test]

public void ShouldHandleAdjustPriceClicked()

{

 decimal percent = 10.0m;

 Expect.Call(view.PriceAdjustPercent)

   .Return(percent.ToString());

 book.AdjustPrice(10.0m);

 view.BindBooks();

 view.InfoMessage = string.Format(

   BooksPresenter.AdjustPriceMessage, 10.0m);

 mocks.ReplayAll();

 presenter.AdjustPriceClicked(null, null);

}

 

The accompanying downloadable code includes examples of unit testing other presentation logic, including server-side validation and formatting the GridView rows when data binding occurs.

 

Considerations

One advantage to testing using mock objects is that unit tests are very fast and can therefore be run often. Figure 4 shows the unit test execution. Note the times in milliseconds.

 


Figure 4: Unit test results and times.

 

Many presentation logic unit tests will be simple and you may wonder if they are really adding value to your project as you write them. Be assured that these tests will often prove their worth during regression testing after making changes or adding features to the code.

 

A final consideration is determining of which implementation details the presenter is aware. It is possible to keep the presenter unaware of the underlying platform. In theory, you could change platforms from ASP.NET to Windows Forms and not have to change any code in the presenter. Or you can take the approach that the presenter can reference platform-specific details like the example for this article. The choice depends on the project and your requirements. If there are no plans to support multiple platforms, the presenter unit tests will have greater coverage when the presenter is platform-aware.

 

Conclusion

Take advantage of the declarative data binding functionality in ASP.NET 2.0 without losing all unit test coverage of your presentation logic. Factoring out presentation logic into unit-testable presenter classes facilitates a balance between no unit tests for code-behind and writing large amounts of manual data binding code.

 

The source code for this article is available for download in both C# and VB.NET.

 

Nicholas Roeder has been developing software professionally for 10 years. He has focused on applying object-oriented design principles to .NET Windows and Web development for the past three years. He received his BSc and MSc degrees in computing science from the University of Alberta, Canada. He lives in Calgary, Alberta, and can be reached at mailto:[email protected].

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish