Wednesday, September 19, 2007

HttpInterfaces for ASP.NET 1.1

Inspired by Phil Haack's HttpInterfaces, I wrote a similar set of interfaces for ASP.NET 1.1 (which is what we still use at work, sigh...), so we can better test our huge legacy codebase. The most significant difference is that DuckTyping doesn't work in 1.1 AFAIK... so I had to write adapter classes to wrap System.Web.HttpApplication, etc, which was pretty trivial thanks to ReSharper.

Let's see how we could use these interfaces to test a common legacy WebForm. Suppose you have a page which puts the content of a QueryString parameter in a Label, i.e.:

 

public class MyPage : Page
{    
  protected Label Label1; 
  private void Page_Load(object sender, EventArgs e) {        
  	Label1.Text = Request.QueryString["text"];    
  } 
    
  protected override void OnInit(EventArgs e) {        
  	InitializeComponent();        
  	base.OnInit(e);    
  } 
  
  private void InitializeComponent() {
  	this.Load += new System.EventHandler(this.Page_Load);    
  }
}

We build a BasePage from which MyPage will inherit, that will allow injection of Request, Response, etc:

 

public class BasePage : Page
{    
  private IHttpRequest requesto;    
  public new IHttpRequest Request {        
    get {            
      if (requesto == null)
        requesto = new HttpRequestAdapter(HttpContext.Current.Request);            
        return requesto;        
    }        
    set { requesto = value; }    
  } 
  ...
}

Make MyPage inherit from BasePage instead of Page, and set the Request to whatever you like... Using this, we can test that Label1 effectively gets the QueryString parameter. Just create a stub for the request, assign it to a instance of MyPage, and call Page_Load(). For example:

 

[Test]
public void PageLoad() {
  MockRepository mocks = new MockRepository();
  IHttpRequest req = (IHttpRequest) mocks.CreateMock(typeof (IHttpRequest));
  string text = "hello world";
  NameValueCollection queryString = new NameValueCollection();
  queryString["text"] = text;
  Expect.Call(req.QueryString).Return(queryString);
  mocks.ReplayAll(); MyPage p = (MyPage) ReflectionHelper.CreatePageWithControls(typeof (MyPage));
  p.Request = req;
  ReflectionHelper.InvokeMethod(p, "Page_Load", null, null);
  Label Label1 = (Label) ReflectionHelper.GetPageControl(p, "Label1");
  Assert.AreEqual(text, Label1.Text);
}

Here I used Rhino.Mocks to create the request stub. Note that we have to use reflection to call Page_Load() and get the controls since they are not public... But with minimum changes to the code, we gained a lot of testability!

With TypeMock, a very powerful mocking framework (although not free), we could write the same test without depending on any interface and without making any changes to the original code:

[Test]
public void PageLoad_WithTypeMock() {
    MockManager.Init();
    Mock requesto = MockManager.Mock(typeof (HttpRequest), Constructor.Mocked);
    string text = "hello world";
    NameValueCollection queryString = new NameValueCollection();
    queryString["text"] = text;
    requesto.ExpectGet("QueryString", queryString);
    Mock myPage = MockManager.Mock(typeof (MyPage), Constructor.NotMocked);
    myPage.ExpectGet("Request", new HttpRequest(null, null, null));
    myPage.Strict = false; 


    MyPage p = (MyPage) ReflectionHelper.CreatePageWithControls(typeof (MyPage));
    ReflectionHelper.InvokeMethod(p, "Page_Load", null, null);
    Label Label1 = (Label) ReflectionHelper.GetPageControl(p, "Label1");
    Assert.AreEqual(text, Label1.Text);
}

But, like I said, TypeMock is not free. There is a community edition, though. I think TypeMock is great for initial legacy testing, but in the long run, it pays off to refactor, to add new seams so that legacy code stops being legacy. The beauty of refactoring is that it can be done progressively, so don't be afraid to make changes!

Oh, I almost forgot, here's the code, have fun! :-)

No comments: