Testing fun with ASP.NET MVC
So I assumed that much like MonoRail, ASP.NET MVC would have created, at the very least, abstractions on top of HttpContext. Although HttpContext is an absolute beast of a god class, creating an abstraction is possible. Let’s look at MonoRail’s approach:
public interface IRailsEngineContext : IServiceContainer, IServiceProvider { // Methods T GetService<T>(); void Transfer(string path, bool preserveForm); // Properties string ApplicationPath { get; } string ApplicationPhysicalPath { get; } ICacheProvider Cache { get; } IServiceProvider Container { get; } Controller CurrentController { get; set; } IPrincipal CurrentUser { get; set; } Flash Flash { get; } IDictionary Items { get; } Exception LastException { get; set; } NameValueCollection Params { get; } IRequest Request { get; } string RequestType { get; } IResponse Response { get; } IServerUtility Server { get; } IDictionary Session { get; } ITrace Trace { get; } HttpContext UnderlyingContext { get; } string Url { get; } UrlInfo UrlInfo { get; } string UrlReferrer { get; } }
Things are looking okay, most objects are hidden behind an interface. Those interfaces that have members that expose other classes, are interfaces as well. Only the items important to a Controller action are present, and the original HttpContext is still present. But ideally, you can go off of the interfaces, IResponse, IRequest, etc. This makes testing MonoRail items (controllers, filters, etc.) straightforward.
Now, let’s take a look at ASP.NET MVC:
public abstract class HttpContextBase : IServiceProvider { // Methods protected HttpContextBase(); public virtual void AddError(Exception errorInfo); public virtual void ClearError(); public virtual object GetGlobalResourceObject(string classKey, string resourceKey); public virtual object GetGlobalResourceObject(string classKey, string resourceKey, CultureInfo culture); public virtual object GetLocalResourceObject(string virtualPath, string resourceKey); public virtual object GetLocalResourceObject(string virtualPath, string resourceKey, CultureInfo culture); public virtual object GetSection(string sectionName); public virtual object GetService(Type serviceType); public virtual void RewritePath(string path); public virtual void RewritePath(string path, bool rebaseClientPath); public virtual void RewritePath(string filePath, string pathInfo, string queryString); public virtual void RewritePath(string filePath, string pathInfo, string queryString, bool setClientFilePath); // Properties public virtual Exception[] AllErrors { get; } public virtual HttpApplicationStateBase Application { get; } public virtual HttpApplication ApplicationInstance { get; set; } public virtual Cache Cache { get; } public virtual IHttpHandler CurrentHandler { get; } public virtual RequestNotification CurrentNotification { get; } public virtual Exception Error { get; } public virtual IHttpHandler Handler { get; set; } public virtual bool IsCustomErrorEnabled { get; } public virtual bool IsDebuggingEnabled { get; } public virtual bool IsPostNotification { get; } public virtual IDictionary Items { get; } public virtual IHttpHandler PreviousHandler { get; } public virtual ProfileBase Profile { get; } public virtual HttpRequestBase Request { get; } public virtual HttpResponseBase Response { get; } public virtual HttpServerUtilityBase Server { get; } public virtual HttpSessionStateBase Session { get; } public virtual bool SkipAuthorization { get; set; } public virtual DateTime Timestamp { get; } public virtual TraceContext Trace { get; } public virtual IPrincipal User { get; set; } }
Still very similar, but this time it’s an abstract class. But a very strange abstract class – it has no abstract members! All members are virtual. Well that was nice of them, providing a default implementation that can be overridden for testing. Ah, but what’s the default implementation? “throw new NotImplementedException()”. For every. single. member. Brilliant!
Next, let’s try to actually test using this class. Abstract base classes minefield base classes (touch it and it asplodes!) are exposed for the various items we might care about, including Request, Response, Session, Server, Trace, Application. Wait, go back one. Trace exposes TraceContext.
Which is not abstract.
Which is sealed.
Kaboom!
I still like creating application-specific wrappers on all of these items. But when you have to create things like filters, it can be painful to work around these limitations.