Coding and Dismantling Stuff

Don't thank me, it's what I do.

About the author

Russell is a .Net developer based in Lancashire in the UK.  His day job is as a C# developer for the UK's largest online white-goods retailer, DRL Limited.

His weekend job entails alternately demolishing and constructing various bits of his home, much to the distress of his fiance Kelly, 3-year-old daughter Amelie, and menagerie of pets.

TextBox

  1. Fix dodgy keywords Google is scraping from my blog
  2. Complete migration of NHaml from Google Code to GitHub
  3. ReTelnet Mock Telnet Server à la Jetty
  4. Learn to use Git
  5. Complete beta release FHEMDotNet
  6. Publish FHEMDotNet on Google Code
  7. Learn NancyFX library
  8. Pull RussPAll/NHaml into NHaml/NHaml
  9. Open Source Blackberry Twitter app
  10. Other stuff

Unit testing WebForms With a Nod Towards MVC and MVP

Hi all,

For those of you who've had a read of my last post, I'm afraid it's enough of the gadget hackery and back to coding!  This time, I'm going to reflect on some work that I'm involved in with my sterling team-dudes in the office.

This last two weeks, we've been migrating chunks of a classic WebForms app to an MVP structure, with an eye to ultimately migrating across to MVC.  This post is going to look at the very first stage in this process - getting our UI and non-UI code split between a View and combined Presenter/Model, so that we can get this code under test.  Future posts are (hopefully) going to look at how we get clean up the messy combined Presenter/Model to leave a properly structured set of Presenters and Models, and how these should and should not be coupled to one-another.

Where We Are Now

We've had some minor dealings with MVC, we've written a few single-ish-page sites using it, and it's dove-tailed really neatly with our work to start embedding sound agile and test-driven practices.  But now that we've had these MVC dealings, we look at our monolithic 62-project enterprise management web app, and we see contentinent-sized chunks of un-testable code buried in our page code-behinds. Not good.

To be fair, our current system was started off almost 3 years ago, to replace an existing system that itself was a good 6 years old, so it's no suprise that MVC passed us by.  Our team's also grown considerably since, so we've got a lot more freedom now than we would have had then.

With WebForms, to date we've found the following problems:

  1. Testing is tough - We've not been disciplined / sensible enough to keep our code-behind files to the bare minimum, this is all stuff we can't hook up in a test.
  2. Refactoring is tough - Because we've got big chunks of code in our code-behinds not under test, we're loathe to touch them needlessly, so refactoring doesn't really happen.
  3. Fragility - because we're not refactoring our code, when we need to fix bugs we often do it in an inelegant, brittle fashion.  We're getting into spaghetti-ville.

Where We Want to Be

It's therefore been decided amongst the team that we need a way out, we need to somehow get from WebForms to MVC (or as close as we can get!).

A full MVC migration would allow us to unit-test pretty much anything.  We can hook up a controller in a suitably isolated unit test, we can run it, we can even dig into the HTML output if we need.  So of course this is our best-case scenario, but how the heck do you migrate in one step from WebForms to MVC?  This sounds like too much to chew to me!

Partial MVC migration is another option, we look at adding new content in MVC alongside our existing WebForms content.  I suspect (but I've not proven) that the two can live side-by-side in a single web project. But this still leaves our existing WebForms code as is, and doesn't give us a good route to migrate away.

So our third option, which we're trialling now, is to migrate from WebForms to MVP WebForms, and then it should be a much more manageable jump from MVP WebForms to MVC.

Our Starting Code

Login.aspx.cs
public partial class Login : System.Web.UI.Page, ILoginView
{
  private LoginPresenter _presenter;
  protected void Page_Init(object sender, EventArgs e)
  {
    _presenter = new LoginPresenter(this);
  }

  protected void btnSendMessage_Click(object sender, EventArgs e)
  {
    // Code goes here to handle login
    // Lots of stuff here that can't be tested
  }
}

Part One - The Cleave

We're going to iterate an overall process, starting with the smallest elements and working our way out - so we start with the inner-most user controls, then the outer-most user controls, then the pages, and finally master pages.

For each element, we want to do two things.  Our first step is to cleave off all of the non-UI stuff, and place this in a new class which will become a combined Model/Presenter (though we'll just call it the Presenter for now as this is hopefully what it will become).  For the following example, assume that I have a page called "login.aspx" and a code-behind called "login.aspx.cs":

  1. Let's create a presenter to hold all of the non-UI stuff, we'll call this "LoginPresenter".
  2. Your view needs to be able to talk to the presenter, so in your Login.aspx.cs in your "Page_Init" event handler, add a constructor for your LoginPresenter.
  3. Now your presenter needs to be able to talk to the view.  We'll do that by injecting the LoginView into your LoginPresenter's constructor.  But remember, we need to be able to unit test the stuff in our presenter, so:
    1. Create an empty interface (ILoginView) and apply this to your class in Login.aspx.cs.
    2. Add a constructor to your LoginPresenter class that takes an IViewLogin parameter, and assigns it to a private class level variable (I'll call this "_view").
    3. When constructing your presenter from your Login.aspx.cs class, you can now pass in a reference to "this".
  4. Identify any methods in your view that do not have any user-interface dependencies:
    1. Move these to your LoginPresenter class as public methods.
    2. If you need access to page-level variables, you'll need to expose these through your IPageView interface so that you can still access them.
    3. Where you were calling these methods from your view before, you can now just prefix them with "_presenter." and they should continue to work.
  5. Now identify any methods in your view that are mostly UI code with small pieces of non-UI code:
    1. Extract each of the small pieces of non-UI code into new individual methods
    2. Move these methods from your Login.aspx.cs class to your LoginPresenter class, as you did with your non-UI methods.
  6. Finally, identify any methods in your view that are mostly non-UI code but with small pieces of UI interaction:
    1. Extract each of the small pieces of UI code into new individual methods on your Login.aspx.cs class, so that the original method contains no UI code.
    2. You can now move the original method down into your LoginPresenter class, of course you'll get compiler errors relating to the small UI methods you've left in your view.
    3. In order to call these UI methods from your LoginPresenter, you'll need to make sure you add the method to your ILoginView interface, then you can simply call "_view.MethodName()" to get your original behaviour back.

The Resuling Code

Login.aspx.cs
public interface ILoginView
{ }

public partial class Login : System.Web.UI.Page, ILoginView
{
  private LoginPresenter _presenter;
  protected void Page_Init(object sender, EventArgs e)
  {
    _presenter = new LoginPresenter(this);
  }

  protected void btnSendMessage_Click(object sender, EventArgs e)
  {
    _presenter.Login(txtUserName.Text, txtPassword.Text, cbForgotten.Checked);
  }
}
LoginPresenter.cs
public class LoginPresenter
{
  private ILoginView _view;
  public LoginPresenter(ILoginView view)
  {
    _view = view;
  }
  public void Login(string username, string password, bool passwordForgotten)
  {
    // Code goes here to handle login
    // Lots of stuff that we can now test
  }
}

Part Two - Test Coverage

You should by now have all your non-UI code in a file called LoginPresenter, and you've left only the bare minimum UI-dependant code in your Login.aspx.cs. We now need to get all that code in our LoginPresenter class under test, which will put us in a really good place to refactor in the future towards a REAL LoginPresenter class, and a REAL LoginModel class (I'll talk more about the Model bit in a moment).

I'm not going to go into great detail here, I assume you guys know how to write unit tests!  But in general that is how we've approached this:

  1. Work on one method at a time
  2. Hook up the method in a test harness, do it as simply as possible, just pass in nulls or 0 or false or whatever, you don't need to look at the code at all yet.
  3. Try to get your test to pass, if you can good job, NOW you can look inside the method under test and see how much of it's being hit by your test (your test coverage, look up PartCover to help with this stuff)
  4. If you can't get your test to pass, you no doubt have dependencies to fix. Things like the arguments passed into your method, things that are being constructed in your methods under test, troublesome class-level members, etc.
  5. If the method under test has arguments that are complicated nested classes, look up the Builder pattern and use it.
  6. If the method under test is creating classes which are hitting external resources (databases, web services, session-state, etc), these will need to be injected as interfaces, so that you can inject mocked or faked versions of these dependencies in your tests.
  7. Repeat the above steps above as many times as it takes so that you have the code in a "vice" - that is you've made the external behaviour of the code imovable.
  8. And finally repeat all of the above steps for each method in your LoginPresenter class.

Remember all the time you're writing tests, your tests are only as good as your asserts. Focus on good quality asserts, and assert against the things that the method does that affect the outside world. For example if you have a method that interacts with a user console display, you'll want to fake user entry coming in from the console, and you'll want to assert the data it writes back to the screen.

Part Three - Splitting Up our LoginPresenter, To Be Continued...

When you've finished the above two steps, you'll have four things:

  • Slimmed down Login.aspx.cs file
  • ILoginView interface to allow the view to be mocked in your LoginPresenter
  • LoginPresenter class with all of the non-UI code that was in your view
  • Tests covering all of the LoginPresenter code and asserting each of the external impact that your methods have.

But this is where I'm a little unsure - I'm reliably advised that the job of the Presenter is to mediate between the Model and the View. Following the single-responsibility principle, this means that all of the leg-work that was moved out of your view will need to move into your model, so that your Presenter is only doing the mediation.

But what is your Model?

On a Login page, is your Model a LoginModel? Or is it a UserModel? A combination of several models, e.g. UserModel, AuthenticationModel, ValidationModel, etc? Do you have a dedicated LoginModel that only exists to aggregate the bits of your application's business layer into a view-friendly format? I've got too many questions in this area for now, but rest assured as I get more experienced with this stuff I'll try to share my findings.


Permalink | Comments (1)

Comments (1) -

David Toon United Kingdom

23 May 2011 13:43

David Toon

I've had a couple of experiences of models becoming quickly bloated - in fact I've wrote an implementation such as this!!! I've wrote this kind of implementation.  Really your Model should have a single responsibility rather than multiple roles, your Presenter might end up coordinating between multiple models.  However, this should probably be a minimum as this will lead to a high degree of "indirect" (I've probably just made this up) coupling between your individual models - i.e. I expect Model A to be called before Model B can be invoked.  

There is probably not a hard and fast rule - but I recall reading (in Growing Object-Orientated Software, by Tests) that if your test code needs a lot of setup before you can test an individual section of code then this is a smell.

I suppose my underlying message is that it depends on the context and what the screen is doing.  If your page just logs in the user then it should probably just log the user in.  

There is a good link here about UI's - which I think is kind of related!  http://www.targetprocess.com/blog/2009/05/race-to-performant-application.html

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading