20100205 Adding a Layer of Abstraction - MichaelKetting/home GitHub Wiki
Published on: Feb 5, 2010 @ 14:50
Adding a Layer of Abstraction
In my last blog post, I talked about the pain a misused singleton can introduce into your development lifecycle, particularly if you're doing test-driven development. But what can you do when the singleton in question isn't under your control, but provided by the framework? And to top it off, it's not even fitted with an interface so you can't mock it. Oh, and in order to instantiate it and use it in your test fixture, you need to resort to reflection.
Well, for years my answer had been to bite the bullet. Accept that the framework isn't suited for a test-first approach. And write reflection-based helpers that allow me to test my most-critical components, at least. Any guesses which real world example I'm talking about?
Enter HttpContext.Current, System.Web.UI.Page, and just basically the entire ASP.NET WebForms stack.
Let me start by listing a couple of scenarios where this is an issue:
- Developing an HTTP handler
A simple handler doesn't even have a user interface, and still, as soon as you need to interact with the request, the response, the session, etc, you're back to fighting in the trenches. There are, of course, ways to get at least some test coverage, but eventually you have to accept that there is a place where no unit test will dare to go. - Developing a custom web control
In addition to dealing with the HttpContext, now you also have to deal with the page reference, the page-lifecycle, protected and internal methods you'd have to call from your tests, etc. Not fun. Not fun at all. - Developing a UserControl or a Page
This is where it get's really interesting---or ugly---and I'm not even going to begin listing the problems you face when trying to unit test those monsters. To put it simply, there's a very good reason why Microsoft developed ASP.NET MVC.
Okay, but what about the first two scenarios? Well, the answer to testing an HTTP handler is surprisingly simple---just introduce a mockable layer of abstraction between your code and the HttpContext. So, why did it take me till summer 2008 to come up with it? Mainly because I had scruples doing something this radical. And no time. See, if you want to add an abstraction for HttpContext, you need to provide delegation for all members. You need to test it. And you need to provide abstractions for all dependent types as well, e.g. HttpRequest, HttpSessionState, etc. Plus documentation, because you don't want to expose the users of your types to the bare metal APIs.
So, what changed in 2008? Easy answer: I realized the infinitive potential of ReSharper's 'Extract Interface' and 'Delegate Members' refactorings. Now all I had to do was create a new type, add a field for my wrapped instance, execute two refactorings with ReSharper, and I had my layer of abstraction. Took me all of thirty minutes or so. Okay, I didn't write tests for it, but I trust ReSharper not to muck up the code generation. Add a bit of plumbing and some null checks, and the entire HttpContext infrastructure is now mockable.
Of course, a few months later we started to depend on .NET Framework 3.5 SP1 in re-motion, and I realized that the old saying about inventions happening at multiple places independently when the time is right still held true. Microsoft had moved the System.Web.Abstractions assembly from the ASP.NET MVC Preview into the core framework and lo and behold, Phil Haack and his gang chose the same approach for ASP.NET MVC's testability. The only difference is that the official stack uses abstract base classes and doesn't expose the wrapped instance. In .NET 4.0, those types will actually get moved into the regular System.Web assembly. And in case you're wondering, my implementation is already earmarked for the big 'Safe Delete' refactoring ;)
This leaves me with control development. Here, you have to distinguish between two basic aspects. One is logic that depends on the correct invocation of methods according to the page lifecycle. In this case, the only testable approach is to gut the control, create some controller classes and generally follow a divide and conquer approach. Once you are able to do this, all that's left of the control is a façade, the properties, and the design-time support.
Much more meatier is the stuff that happens when the control is interacting with the outside, e.g. registering scripts, rendering its contents, etc.
So, ever checked out the mechanism provided by ASP.NET for script registration? The API you're looking for is the ClientScriptManager exposed by the property ClientScript on Page. It was introduced with .NET 2.0, replacing the previously used methods exposed directly on the page. Then, a year or so later, along came the ASP.NET AJAX Extensions, back then a separate download and located in the assembly System.Web.Extensions. Now, if you want to use asynchronous postbacks via the UpdatePanel, you have to use static methods on the ScriptManager to register your scripts. Suffice to say, I was not a happy camper.
This time, the solution was a tad more complicated and intrusive, but it integrated well with a concept introduced into our web-stack back in 2004---interfaces for Control and Page. The original reasoning behind it was to give projects the option of using a custom or third-party layer-super-type and still integrate their pages with re-call, re-motion's control flow architecture for ASP.NET WebForms. In order to achieve this, we merely copied the signatures of all public members into the interface, derived our own interfaces (e.g. IWxePage) and that was it. Of course, this also meant exposing the ClientScriptManager and the HttpContext as is.
The implementation was simple and straight forward, and it served it's designated purpose. But it also failed to open the door to do actual test-first development because I couldn't just mock the Page property on a control, nor the Context property on the page returned by said Page property, not to mention making it possible to test my script registration logic. All these requirements called for a much larger refactoring.
To make a long story short, I introduced an interface and a wrapper for ClientScriptManager, merged the ScriptManager's (relevant) API into this interface and changed the type of the ClientScript property on re-motion's page interface (IPage). Same goes for the Context property and the Page property. And the result of this effort is that I'm now able to actually expect the registration of a specific script when I implement a new feature. The first benefactor of this approach are the web components implemented in re-vision, our document management system.
I will do a separate blog post some time in the future that shows how to leverage our web-stack, but for now the conclusio is that it is now possible to write unit tests for your extensions to re-motion's web-stack.
Michael