Make ASP.NET Web Applications Testable

Professional developers write clean code. Clean code includes maximal test coverage. To maximize test coverage we need testable code. Code behind of ASP.NET Web Forms Applications is very difficult to test.  In this post I want to show how an classical ASP.NET Web Forms Applicaiton can be refactored to be easy testable.

The Code Behind

For this post I prepared a simple ASP.NET Web Forms Application with some commonly used features. The Application consists of a GridView listing a set of customers from the Northwind.sdf sample database file. The customers can be searched by a simple search control. By clicking on a link the detail data of a customer will be shown on a separate page. With another link a customer can even be deleted.

Customers Overview

So the base features of the Web Application are:

  • Show Overview
  • Show Detail Data
  • Delete Customer
  • Find Customer

The sample Application has a CustomerRepository class providing all the data operations. You can download the complete code from GitHub.

Loading Overview Data

With the following two lines of code we can load the data from the repository and show them on the GridView of the main page:

Customers.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
    ...
    this.gvCustomers.DataSource = this.repository.GetAll();
    this.gvCustomers.DataBind();
}

Showing Detail Data

If we want to show the detail Data of a customer we can use the following lines of code:

Customers.aspx
<asp:GridView ID="gvCustomers" ...>
<Columns>
  <asp:TemplateField>
    <ItemTemplate>
      <a href="<%# string.Format("{0}?id={1}", this.ResolveUrl("~/CustomerDetails.aspx") , Eval("Customer_ID")) %>">Details</a>
    </ItemTemplate>
  </asp:TemplateField>
  ...
</Columns>
</asp:GridView>

CustomerDetails.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
    string customerId = Request.Params["id"];
    if (customerId == null)
    {
        return;
    }

    var repo = new CustomerRepository();
    this.dvDetails.DataSource = new[] { repo.Get(customerId) };
    this.dvDetails.DataBind();
}

Deleting Data

For deleting a customer we can use this lines of code:

Customers.aspx.cs
protected void gvCustomers_RowDeleting(object sender, GridViewDeleteEventArgs e)
{
    var customerList = (IReadOnlyList<Customer>)this.gvCustomers.DataSource;
    Customer customerToDelete = customerList[e.RowIndex];

    this.repository.Delete(customerToDelete.Customer_ID);
}

Finding Data

If we want to find a customer we can use this lines of code:

SearchCustomerControl.ascx.cs
public partial class SearchCustomerControl : UserControl
{
    public delegate void PerformSearchByCompanyHandler(object sender, PerformSearchByCompanyEventArgs eventArgs);

    public event PerformSearchByCompanyHandler SearchPerformed;

    protected void btnSearchByCompany_Click(object sender, EventArgs e)
    {
        this.btnClearSearch.Enabled = !string.IsNullOrWhiteSpace(this.txtSearchTerm.Text);

        if (this.SearchPerformed != null)
        {
            this.SearchPerformed(this, new PerformSearchByCompanyEventArgs(this.txtSearchTerm.Text));
        }
    }

    ...
}

Customers.aspx.cs
public partial class Customers : Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        this.SearchCustomerControl1.SearchPerformed += this.SearchCustomerControl_OnSearchPerformed;
        ...
    }

    private void SearchCustomerControl_OnSearchPerformed(object sender, PerformSearchByCompanyEventArgs eventArgs)
    {
        this.gvCustomers.DataSource = this.repository.GetMatching(eventArgs.SearchTerm);
        this.gvCustomers.DataBind();
    }
}

Conclusion

As we can see by these examples the major part of the UI logic is realised in the code behind part of each page respectivelly user controls. How should we test it? As we know each ASP.NET Web Control has a lot of dependencies and is afflicted with deep inheritance. Trying to test the pages and user controls directly has a lot of disatvantages. It obligates us to keep a reference to the System.Web assembly. It also forces us to make a lot of wrapper classes in order to mock static infrastructure classes and so on.

As we reflect all the issues with code behind we realize that we have to get the UI logic out of there. So how can we achieve this goal?

MVP

On possible way to solve our problem is to make use of the Model-View-Presenter (MVP) pattern. In this pattern the presentation logic of the UI is implemented in the presenter rather than in the view itself.

MVP

The presenter is responsible for gathering the data. The data are hosted within the model. The presneter receives the data from the model and “places” them on the view. The view on the other hand receives the user input and passes it back to the presenter. The presenter decides what happens next.

For detail explanations please visit Design Patterns: Model View Presenter.

Refactoring

In order to get testable code we have to prepare a testable code structure first. This is were we introduce view interfaces for the pages and user controls.

View Interfaces

The interfaces are an abstract representation of the view functionality. With view interfaces prepared we will be able to write mocks later for testing purposes. We start with the interface for the overview view.

public interface ICustomersView
{
    void ShowCustomers(IReadOnlyList<Customer> customers);

    IReadOnlyList<Customer> GetCustomers();
}

Important: Do not expose ASP.NET Controls in the View Interace! With this you would introduce again a lot of havy dependencies with the System.Web assembly.

Presenters

Now we can start introducing the presenters. The essential part here is the initialization of the view and the presenter. Because in this sample the presenter “knows” the view and vice versa.

We start with the presenter for the overview data. We give the presenter a logical name like CusomtersPresenter. Next we equip the presenter with a Initialize method from which the view will make use of. And this is how it looks like in the code:

CustomersPresenter.cs
public class CustomersPresenter
{
    private ICustomerRepository customerRepository;
    private ICustomersView view;

    public void Initialize(ICustomersView view, ICustomerRepository customerRepository)
    {
        this.view = view;
	this.customerRepository = customerRepository;
    }
    ...
}

Customers.aspx.cs
public partial class Customers : Page, ICustomersView
{
    private CustomersPresenter presenter;

    public void Initialize()
    {
        this.presenter = new CustomersPresenter();
        this.presenter.Initialize(this, new CustomerRepository());
    }
    ...
}

With this the connection between the view and the presenter is established. Of course this is a really simple example but it does the trick. In a real life application we would make use of an IoC container for dependency injection.

Next we start moving the logic from the code behind of the view to the presenter. We start with the loading of the overview data.

Loading Overview Data

Customers.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
    this.Initialize();
}

public void ShowCustomers(IReadOnlyList<Customer> customers)
{
    this.gvCustomers.DataSource = customers;
    this.gvCustomers.DataBind();
}

CustomersPresenter.cs
public void Initialize()
{
    this.LoadAndShowAllCustomers();
}

public void LoadAndShowAllCustomers()
{
    IReadOnlyList<Customer> customers = this.customerRepository.GetAll();
    this.view.ShowCustomers(customers);
}

As we can see by the example above there is still some code behind in the view left. But this code within the view is just the accessor for the ASP.NET cotrols. The main UI logic is now located in the presenter.

Deleting Data

The next examples makes the improvement even more clear:

Customers.aspx.cs
protected void gvCustomers_RowDeleting(object sender, GridViewDeleteEventArgs e)
{
    this.presenter.DeleteCustomerRow(e.RowIndex);
}

CustomersPresenter.cs
public void DeleteCustomerRow(int rowIndex)
{
    IReadOnlyList<Customer> customerList = this.view.GetCustomers();
    Customer customerToDelete = customerList[rowIndex];

    this.customerRepository.Delete(customerToDelete.Customer_ID);
}

As we can see the view “delegates” the deletion of the customer to the presenter. The presenter knows how to process this request. He prepares the data needed for the next step by creating the model. In this example the model is a Customer object. The

Testable Code

With the newly introduced presenter we start writting tests. For each presenter we create a test class. In this example I use NUnit, FluentAssertions and FakeItEasy. Therefore the setup of the test class looks like the following:

CustomersPresenterTest.cs
[TestFixture]
public class CustomersPresenterTest
{
    private CustomersPresenter testee;

    private ICustomersView customersView;
    private ICustomerRepository customerRepository;

    [SetUp]
    public void SetUp()
    {
        this.customersView = A.Fake<ICustomersView>();
        this.customerRepository = A.Fake<ICustomerRepository();

        this.testee = new CustomersPresenter();
        this.testee.Initialize(customersView, customerRepository);
    }
}

Now we can write the long awaited tests.

Loading Overview Data

We want to test if all the data from the repository are passed over to the view.

CustomersPresenterTest.cs
[Test]
public void LoadsAndShowsAllCustomers()
{
    IReadOnlyList<Customer> customers = new[] { new Customer(), new Customer() };

    A.CallTo(() => this.customerRepository.GetAll()).Returns(customers);

    this.testee.LoadAndShowAllCustomers();

    A.CallTo(() => this.customersView.ShowCustomers(customers)).MustHaveHappened();
}

Deleting Data

We want to test if the right customer will be deleted.

CustomersPresenterTest.cs
[Test]
public void DeletesCustomerRow()
{
    const string SecondCustomerId = "second";
    const int SecondCustomerRowIndex = 1;

    IReadOnlyList<Customer> customers = new[]
    {
        new Customer { Customer_ID = "first" },
	new Customer { Customer_ID = SecondCustomerId },
	new Customer { Customer_ID = "third" }
    };

    A.CallTo(() => this.customersView.GetCustomers()).Returns(customers);

    this.testee.DeleteCustomerRow(SecondCustomerRowIndex);

    A.CallTo(() => this.customerRepository.Delete(SecondCustomerId)).MustHaveHappened();
}

Just with this few tests we improved the code quality of the application decently and gained additional safety. With just one look at the test classes we know the core functionality of our small Web Application.

You can find the whole source code with all test cases on GitHub.

Summary

Classical ASP.NET Web Forms Applications with code behind are hard to test. Therefore we use the MVP pattern to extract the main UI logic from the views and concentrate it into presenters.
Presenters have no dependencies to UI specific frameworks. Therefore we can easily write tests for them. With these tests we can improve our code and develop robust Web Applications.

This entry was posted in .NET, Clean Code, Testing, UI. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *