Saturday 25 August 2007

ASP.Net testability.. finally the way it should be!

Now, a year or two ago I decided that all my Web apps should be test-driven too. So, my only option at this point was NUnitAsp (which is great). Basically, it initiated a request, parsed the output, and you could test the properties of your controls. It even allowed you to click buttons or links and process the resulting request. Pretty cool.

There are several problems with this approach. First, these tests are really integration tests. There is no way to mock anything. So, before using any test data, for example, I had to put it into the database. And delete it after each test, making sure it's deleted even if the test fails. Or, when testing the logged in behavior, I had to navigate to the login page, enter the credentials, and click the button. In short, every test setup became huge. Second, The control tree should be traversed manually, meaning that I had to explicitly write things like myLabelTester = New LabelTester("ctl00:Panel1:Label1"). And third, each control type had to have its own tester (which parsed the rendered html), so for a custom control you had to write your own custom tester. Oh, and last time I checked, they had testers for Asp 1.1 version only, and even a DataList tester was missing. You can guess well that after some time I abandoned the idea because writing tests was too painful.

The next thing was Plasma. It also returns the html from your requests and suggests that you parse it (it even doesn't have all the convenience parsers/testers that NUnitAsp has), but there's a big difference. It runs everything in-process. Meaning that whenever I run my favourite test runner, the objects are already there somewhere. I just need to find them.

Basically, I don't want to get an html string from my test and just make sure that it contains "Hello world". I'd like to write something like this:

Dim page As Web.UI.Page = ExecuteRequest("~/Default.aspx")

Assert.AreEqual(1, page.Controls.Count)

Dim label As Web.UI.WebControls.Label = page.FindControl("Label1")

Assert.AreEqual("Hello world", label.Text)

In addition, now I can (theoretically) mock my dependencies. The only problem is that Plasma provides just the WorkerRequest object and the html response body. There's no way I could get to the Page object, not even with some dirty Reflection tricks.

Fortunately, we also have TypeMock, which can do even dirtier. It can set a mock on a type, so that when an object of that type is instantiated, we can get a reference to it, intercept all method and property calls etc.

There are just two minor obstacles left. First, the mock should be created in the AppDomain that is created by Plasma, meaning that I can't just create them in my tests -- I should modify the Plasma code (actually, I don't need Plasma -- there are several examples of hosting Asp.Net out there (see the references at the bottom)). Not much of an obstacle -- I'm going to put all the relevant code just before the request is executed. Second, we don't know the actual type of our page, so we can't mock it. The solution is to mock the HTTPContext object and get the page reference from it at the point it is assigned the Handler property:

System.Web.UI.Page page; //keep the global page variable

internal int ProcessRequest(

string requestFilePath,

string requestPathInfo,

string requestQueryString,

string requestMethod,

List<KeyValuePair<string, string>> requestHeaders,

byte[] requestBody,

out List<KeyValuePair<string, string>> responseHeaders,

out byte[] responseBody) {

WorkerRequest wr = new WorkerRequest(requestFilePath, requestPathInfo,

requestQueryString, requestMethod, requestHeaders, requestBody);

//begin our code

TypeMock.MockManager.Init();

TypeMock.Mock contextMock = TypeMock.MockManager.Mock(typeof (System.Web.HttpContext), TypeMock.Constructor.NotMocked);

contextMock.MockMethodCalled += new TypeMock.MockMethodCalledEventHandler(contextMock_MockMethodCalled);

contextMock.ExpectUnmockedSet("Handler");

//end our code

HttpRuntime.ProcessRequest(wr);

while (!wr.Completed) {

Thread.Sleep(50);

}

responseHeaders = wr.ResponseHeaders;

responseBody = wr.ResponseBody;

return wr.ResponseStatus;

}

void contextMock_MockMethodCalled(object sender, TypeMock.MockMethodCallEventArgs e)

{

if (e.CalledMethodName == "set_Handler") page = (System.Web.UI.Page)e.SentArguments[0];

}



While this is still far from something I could really use in my tests, the main point is already here. By the time we get to the end of the ProcessRequest method, we have a page variable that is really our page, so we can test all its controls, not just the html output.

I think it's worth of a really powerful framework, with Enterprise (I like the word!) features like IHTTPModule testing, built-in authentication and config mocking etc.

4 comments:

Anonymous said...

Hi Ulu,

I was just wondering if you have made progress with using Typemock and Plasma. I have been thinking
about the same thing but it seems to me I will have problems with cross AppDomain mocking issues. You comment on AppDomain issues but I didn't understand your solution.

I was hoping to use WebAii, mbUnit and Typemock.

To me, making this work would be incredibly useful, however you are the only person I have found that seems to be talking about such a technique. Others are taling about TDD and mocking, and some are talking about automated UI testing with Cassini, Watin or WebAii but nobody is tying the two together.

Many thanks

Graham

ulu said...

Hi Graham,

It's great that somebody's interested in this idea. Actually I'm very close to delivering a working product. It's called Ivonna, and the ETA is about two weeks.

The solution to the AppDomain problem is that you have to create your fixture class in the same AppDomain as the Asp.Net lives. After that, you mock the HTTPContext using TypeMock and handle the MockMethodCalled event. Depending on which method/property you are mocking, you can inject your code into any point of the request lifecycle.

I used Plasma as my starting point, but later I discovered a lot of examples of how to host the Asp.Net process, so I discarded Plasma. I still depend on TypeMock, since it is a vital part of "hacking" the Asp.Net framework.

Getting a page was relatively easy; the toughest part was implementing postbacks.

Ulu

Anonymous said...

Hi Ulu,

Thanks for getting back to me. What you describe sounds interesting. I'll watch out for more details.
Cheers

Graham

ulu said...

The beta version of Ivonna is going to be released pretty soon. You can watch it at http://sm-art.biz/Ivonna.aspx.

You can also register using the link at the bottom and subscribe for notifications at the Announcements forum.