Codementor Events

Unit testing Asp.Net Web Forms legacy applications

Published Feb 10, 2019

I have seen a lot of Asp.Net Web Forms legacy applications for which I have been told that they cannot be unit tested and that’s the reason why the tests are missing. I have seen a lot of pages with hundreds or even thousands of lines of code and event handlers in which you could scroll forever for their end. This approach, probably it’s a legacy issue, but exists and definitely it’s not the way to go due to the followings:

  1. When the functionality changes, the risk to introduce new bugs is high
  2. The debugging is time consuming since the only way to run that code is to start the application in browser and to navigate to needed page. Also the data context needs to be set properly which takes time.
  3. A new programmer needs to understand all the code in order to be able to make a change
  4. Business logic is coupled with UI so every change to one “layer” could imply changes to the other “layer”.

That’s why another approach is needed and this approach should imply the creation of the unit tests. In order to introduce these, we need to decouple the layers and to create proper business logic which is testable and let the UI layer for integration tests. The architectural design which is used is called MVP – Model-View-Presenter.

Goals

  1. We want to be able to create unit tests for business logic
  2. We want to create component tests by linking together business logic and data access
  3. We want to minimize the debugging time by running a specific part of the application without configuring all it’s data context.
  4. We want to have a better state of mind AFTER we make a change to an existing behavior.
  5. We want to enhance the overall quality of the code.
  6. We want to have better marks in SonarQube for our application.
  7. We want to make a step forward continuous delivery(CI).

MVP is an architectural design depicted by the following diagram:

Model_View_Presenter_GUI_Design_Pattern

The View is responsible for displaying the information to the end user and in case of Asp.Net Web Forms, this layer is represented by the markup pages(*.aspx) and the code-behind(*.aspx.cs). The only responsibility is to display the Model returned by the Presenter and NO business logic should be applied here.

Each View has one corresponding presenter which is responsible for managing the business logic operations and forwarding the requests to data access layer. The Presenter responsibilities are the followings:

  1. Collect all input from the View
  2. Call Data Access layer
  3. Creates the Model and sent it to View by:
  4. Returning to the view the results obtained from Data Access layer
  5. Transforming results from Data Access layer and return them to the View

The Model represents data which is got from Data Access and/or is transformed by the Presenter.

Data Access could be represented by the Database layer or, in case of a SOA architecture, by Web Services/Rest Services.

Practical approach

Let suppose we have one page called Default.aspx, with the following markup:

<asp:Content ID=”BodyContent” ContentPlaceHolderID=”MainContent” runat=”server”>

<asp:Button runat=”server” ID=”btnExecute” Text=”Execute” OnClick=”btnExecute_Click” />

</asp:Content>

And the code-behind:

public partial class _Default : Page
{ 
  protected void Page_Load(object sender, EventArgs e) 
    {
    	ExecuteLogic1; 
        ExecuteLogic2; ... AnotherPartOfLogic1 
        Repository.GetDataAccessData(); 
        ExecuteLogicN; 
     }
     protected void btnExecute_Click(object sender, EventArgs e) 
     {
     	ExecuteLogic1; ExecuteLogic2; ... 
        Repository.GetDataAccessData();
        ExecuteLogicN;
     }
}

Let suppose the followings:

  1. .N are not methods but functionalities which do something but are scrambled across Page_Load() method or btnExecte_Click().
  2. There is a mix of data access calls and business logic
  3. The UI controls are referenced for getting/setting their values.
  4. Session/ViewState/Application/QueryString variables or wrappers are used inside these logics.
  5. Data access calls are made through a static class called Repository which has just static methods.

Introduce MVP

We setup the context or the initial state of the page, let’s proceed to introducing MVP, for now we should not change almost anything to the code. We need to create a new folder in where to put all the business logic classes which we will create and we will name this folder “Presenters” since Presenter classes will reside here.

Step1

The following operations need to be done:

  1. Create a new interface called IDefaultPresenter
  2. Create a new class called DefaultPresenter which implements IDefaultPresenter
  3. Create a new class called PresentersFactory in where presenter’s instances are created.
  4. Add in _Default.aspx.cs the aggregation for IDefaultPresenter and get the instance from The instantiation could be done in constructor or in page events.
  5. Create new methods in IDefaultPresenter which correspond with the Page_Load() and btnExecute_Click()
  6. In DefaultPresenter, copy all the code from Page_Load() and btnExecute_Click().
  7. Replace the code from Page_Load() and btnExecute_Click() with the calls to their corresponding methods from presenter.
  8. Compile the code in order to identify the following step which is needed to be done.

The _Default.aspx.cs would look something similar with the following:

public partial class _Default : Page 
{ 
  private IDefaultPresenter _presenter; 
    public _Default() 
    {
    	_presenter = PresentersFactory.GetPresenter();
    }
    protected void Page_Load(object sender, EventArgs e)
    {
    	_presenter.ExecutePageLoad();
    } 
    protected void btnExecute_Click(object sender, EventArgs e) 
    {
    	_presenter.ExecuteBtnExecuteHandler();
    }
}

Step2

The last operation from Step1 was to recompile the code so we saw where further changes are required. The first thing we saw is that the UI components are not in their place and we need to move them back to code behind. The following operations are needed:

  1. For every UI control or UI section, extract the code which handles it and create a new method.
  2. Create if not exists already, model classes/properties for every UI control/section.
  3. Adjust the signature of newly created methods with the Model classes.(add Model class instance as method’s argument).
  4. Modify the methods in order to work only with Model class instance
  5. Move created methods in the aspx.cs.
  6. Adjust the IDefaultPresenter interface with the methods responsible for creating the Model instances.
  7. Remove not needed methods from IDefaultPresenter/DefaultPresenter
  8. Execute the same with all UI controls/sections

Current state of the application looks like below:

public partial class _Default : Page 
{
  private IDefaultPresenter _presenter;
    public _Default() 
    {
    	_presenter = PresentersFactory.GetPresenter();
    }
    protected void Page_Load(object sender, EventArgs e) 
    {
     	SetupExecuteButton(_presenter.GetExecuteButtonModel());
    }
    protected void btnExecute_Click(object sender, EventArgs e) 
    {
     	_presenter.ExecuteBtnExecuteHandler();
    }
    private void SetupExecuteButton(ExecuteButtonModel model) 
    {
     	btnExecute.Text = model.ExecuteText(); 
    }
}
public class DefaultPresenter : IDefaultPresenter 
{
  public void ExecuteBtnExecuteHandler()
    {
    	ExecuteLogic1; ExecuteLogic2; . . . 
        Repository.GetDataAccessData() 
        ExecuteLogicN; 
    }
    public IModel GetExecuteButtonModel() 
    {
    	return new IModel(); 
    }
}

Step 3

Once the Step 2 is finished, the Presenter should have methods responsible for creating or transporting Model instances from UI level. We split the UI layer from Business Logic layer and introduced a new level of abstraction – the Model.

We recompile again and we saw that there are compilation errors which points to the Data access mechanism(Or we don’t have errors BUUUT we use static calls to Repository methods). We need to get  rid of compilation errors or static Repository calls. We need to create new Repository entities in order to minimize the impact on the entire project.

  1. Create a new interface and an implementation for Repository
  2. In implementation use the static calls to Data Access which already exist.
  3. Add in Repository interface the methods used in Presenter
  4. Create a Repository factory for building instances
  5. Inject in Presenter, in constructor, the dependency to Repository.
  6. Replace all calls to static Repository methods with the newly added dependency instance method calls.
  7. Modify PresentersFactory to include added dependency.

Current state of the application looks like below:

public class DefaultPresenter : IDefaultPresenter 
{
  private readonly IRepository _repository;
    public DefaultPresenter(IRepository repository)
    {
    	_repository = repository;
    }
    public bool ExecuteButtonHandler(ExecuteButtonModel executeButtonModel)
    {
    	ExecuteLogic(); 
        _repository.CallDataAccess(executeButtonModel);
        ExecuteLogic();
        return true;
    }
    public IModel GetExecuteButtonModel() 
    {
    	return new IModel();
    }
}
public partial class _Default : Page 
{
  private IDefaultPresenter _presenter;
    public _Default() 
    {
    	IRepository repository = RepositoryFactory.GetRepository();
        _presenter = PresentersFactory.GetPresenter(repository);
    }
    protected void Page_Load(object sender, EventArgs e) 
    {
    	SetupExecuteButton(_presenter.GetExecuteButtonModel()); 
    }
    protected void btnExecute_Click(object sender, EventArgs e) 
    {
    	ExecuteButtonModel model=new ExecuteButtonModel();
        model.Prop1 = UIControl.SelectedValue;
        bool result = _presenter.ExecuteButtonHandler (model); 
    }
    private void SetupExecuteButton(ExecuteButtonModel model) 
    {
    	btnExecute.Text = model.ExecuteText(); 
    }
 }

Step 4

When we reach this step, the UI layer is separated from Business Logic layer and also we have a decoupled Data Access layer.  The compilation errors should be fixed and the page should be up and running.

We prepared the ground for introduction of Unit Tests since now we have separate layers with injected dependencies and we can test individual features or even we can test the Business Logic in relation with Data Access. Once we have a bunch of unit tests and we have a code coverage good enough, we can proceed further with refactorings. Until then, I recommend making just mandatory needed amount of changes and let the code as it was with all it’s tangled.

  1. Create new Project of type Unit Test
  2. Add the reference to Asp.Net Web Forms application
  3. Create unit tests using well known third parties like Moq. Moq can be managed by NuGet and it’s the most popular framework for .Net testing.

An example of Unit Test is the below one:

[TestClass] public class DefaultPresenterTests 
{ 
  [TestMethod] 
  public void GetExecuteButtonModel() 
  { 
    	Mock<IRepository> repositoryMock = new Mock<IRepository>(); 					repositoryMock.Setup(p => p.Method1(It.IsAny<string>())).Returns((string x) => { return x; }); 
    	IDefaultPresenter presenter = new DefaultPresenter(repositoryMock.Object);
        IModel model = presenter.GetExecuteButtonModel(); 								Assert.IsNotNull(model); 
    } 
}

Drawbacks

 As I mentioned before, this operation is not quite a refactoring because, by definition, it requires to not alter the behavior of the application but since there are no unit tests, we can’t know for sure that we didn’t break anything. That’s why the described approach is done incremental, one page at a time.

It could take some time even to be able to compile successfully the application, until Step 3 is not ready, the compilation will fail.

It takes some time to transform an entire application to MVP.

Conclusion

Even if this approach has it’s drawbacks, once the operation is finished, the results are beautiful. The layers are not coupled, no static methods are used, individual testing of a feature it is possible and also the code is MUCH MORE readable and I think it can be understood better.

Debugging is easier to do since the programmer can debug a test and check step by step the correctness of Business Logic and/or Data Access.

The gained readability is a very important feature which improves the development speed of the entire application.

Discover and read more posts from vlad daraban
get started
post commentsBe the first to share your opinion
Show more replies