How to mock application path when unit testing Web App
I am testing code in a MVC HTML helper that throws an error when trying to get the application path:
//appropriate code that uses System.IO.Path to get directory that results in:
开发者_如何学运维string path = "~\\Views\\directory\\subdirectory\\fileName.cshtml";
htmlHelper.Partial(path, model, viewData); //exception thrown here
The exception that is thrown is
System.Web.HttpException: The application relative virtual path '~/Views/directory/subdirectory/fileName.cshtml' cannot be made absolute, because the path to the application is not known.
Following the advice of How to resolve issue with image path when testing HtmlHelper?
I have faked (using Moq):Request.Url
to return a stringRequest.RawUrl
to return a stringRequest.ApplicationPath
to return a stringRequest.ServerVariables
to return a null NameValueCollectionResponse.ApplyAppPathModifier(string virtualPath)
to return a string
What else is needed to be able to allow this code to run in the context of a unit test run?
Or What other approach should I be taking to render a Partial view on a dynamically built string?As an alternative to mocking built-in .net classes, you can
public interface IPathProvider
{
string GetAbsolutePath(string path);
}
public class PathProvider : IPathProvider
{
private readonly HttpServerUtilityBase _server;
public PathProvider(HttpServerUtilityBase server)
{
_server = server;
}
public string GetAbsolutePath(string path)
{
return _server.MapPath(path);
}
}
Use the above class to get absolute paths.
And for For unit testing you can mock and inject an implementation of IPathProvider that would work in the unit testing environment.
--UPDATED CODE
For what it's worth, I ran up against the same error and followed it through the System.Web
source to find it occurs because HttpRuntime.AppDomainAppVirtualPathObject
is null.
This is an immutable property on the HttpRuntime singleton, initialized as follows:
Thread.GetDomain().GetData(key) as String
where key is ".appVPath"
. i.e. it comes from the AppDomain. It might be possible to spoof it with:
Thread.GetDomain().SetData(key, myAbsolutePath)
But honestly the approach in the accepted answer sounds much better than mucking around with the AppDomain.
I'm including a solution from a blog post, which is no longer available (http://blog.jardalu.com/2013/4/23/httprequest_mappath_vs_httpserverutility_mappath)
Complete code: http://pastebin.com/ar05Ze7p
Ratna (http://ratnazone.com) code uses "HttpServerUtility.MapPath" for mapping virtual paths to physical file path. This particular code has worked very well for the product. In our latest iteration, we are replacing HttpServerUtility.MapPath with HttpRequest.MapPath.
Under the hoods, HttpServerUtility.MapPath and HttpRequest.MapPath are the same code and will result in the same mapping. Both of these methods are problematic when it comes to unit testing.
Search for "server.mappath null reference" in your favourite search engine. You are going to get over 10,000 hits. Almost all of these hits are because test code calls HttpContext.Current and HttpServerUtility.MapPath. When the ASP.NET code is executed without HTTP, HttpContext.Current will be null.
This issue (HttpContext.Current is null) can be solved very easily by creating a HttpWorkerRequest and intializing HttpContext.Current with that. Here is the code to do that -
string appPhysicalDir = @"c:\inetpub\wwwroot"; string appVirtualDir = "/"; SimpleWorkerRequest request = new SimpleWorkerRequest(appVirtualDir, appPhysicalDir, "/", null, new StringWriter()); HttpContext.Current = new HttpContext(request);
With that simple code in unit test, HttpContext.Current is initialized. Infact, if you notice, HttpContext.Current.Server (HttpServerUtility) will also be intiailzed. However, the moment, the code tries to use Server.MapPath, the following exception will get thrown.
System.ArgumentNullException occurred HResult=-2147467261 Message=Value cannot be null. Parameter name: path Source=mscorlib ParamName=path StackTrace: at System.IO.Path.CheckInvalidPathChars(String path, Boolean checkAdditional) InnerException: HttpContext.Current = context;
Infact, if the code uses HttpContext.Current.Request.MapPath, it is going to get the same exception. If the code uses Request.MapPath, the issue can be resolved in the unit test easily. The following code in unit test shows how.
string appPhysicalDir = @"c:\inetpub\wwwroot"; string appVirtualDir = "/"; SimpleWorkerRequest request = new SimpleWorkerRequest(appVirtualDir, appPhysicalDir, "/", null, new StringWriter()); FieldInfo fInfo = request.GetType().GetField("_hasRuntimeInfo", BindingFlags.Instance | BindingFlags.NonPublic); fInfo.SetValue(request, true); HttpContext.Current = new HttpContext(request);
In the above code, the request worker will be able to resolve the map path. This is not enough though, because HttpRequest does not have the HostingEnvironment set (which resolves MapPath). Unfortunately, creating a HostingEnvironment is not trivial. So for unit-test, a "mock host" that just provides the MapPath functionality is created. Again, this MockHost hacks lot of internal code. Here is the pseudo-code for the mock host. Complete code can be downloaded here: http://pastebin.com/ar05Ze7p
public MockHost(physicalDirectory, virtualDirectory){ ... } public void Setup() { Create new HostingEnvironment Set Call Context , mapping all sub directories as virtual directory Initialize HttpRuntime's HostingEnvironment with the created one }
With the above code when MapPath is called on HttpRequest by it should be able to resolve the path.
As a last step, in the unit test, add the following code -
MockHost host = new MockHost(@"c:\inetpub\wwwroot\", "/"); host.Setup();
Since now a HostingEnvironment has been initialized, the test code will be able to resolve virtual paths when HttpContext.Current.Request.MapPath method is called (along with HostingEnvironment.MapPath and HttpServerUtility.MapPath).
Download MockHost code here: http://pastebin.com/ar05Ze7p
Trying to make parts of ASP.NET happy with various types of tests seems, to me, to be quite fragile. And I am inclined to believe that the mocking route only works if you basically avoid using ASP.NET or MVC and, instead, write your own webserver from scratch.
Instead, just use ApplicationHost.CreateApplicationHost
to create a properly-initialized AppDomain
. Then run your test code from within that domain using AppDomain.DoCallback
.
using System;
using System.Web.Hosting;
public class AppDomainUnveiler : MarshalByRefObject
{
public AppDomain GetAppDomain()
{
return AppDomain.CurrentDomain;
}
}
public class Program
{
public static void Main(string[] args)
{
var appDomain = ((AppDomainUnveiler)ApplicationHost.CreateApplicationHost(
typeof(AppDomainUnveiler),
"/",
Path.GetFullPath("../Path/To/WebAppRoot"))).GetAppDomain();
try
{
appDomain.DoCallback(TestHarness);
}
finally
{
AppDomain.Unload(appDomain);
}
}
static void TestHarness()
{
//…
}
}
Note: when trying this myself, my test runner code was in a separate assembly from the WebAppRoot/bin
directory. This is an issue because, when HostApplication.CreateApplicationHost
creates a new AppDomain
, it sets its base directory to something like your WebAppRoot
directory. Therefore, you must define AppDomainUnveiler
in an assembly that is discoverable in the WebAppRoot/bin
directory (so it must be in your webapp’s codebase and cannot be stored separately in a testing assembly, unfortunately). I suggest that if you want to be able to keep your test code in a separate assembly, you subscribe to AppDomain.AssemblyResolve
in AppDomainUnveiler
’s constructor. Once your testing assembly gets the AppDomain
object, it can use AppDomain.SetData
to pass along information about where to load the testing assembly. Then your AssemblyResolve
subscriber can use AppDomain.GetData
to discover where to load the test assembly from. (I’m not sure, but the sort of objects you can SetData
/GetData
might be quite limited—I’ve just used string
s myself to be safe). This is a bit annoying, but I think it is the best way to separate concerns in this situation.
This happens once you login to the application and you try to add any new url to the http context and trying to create SimpleWorkerRequest.
in my case i have an url to get the documents from remote server and added the url to http context and trying to authenticate the user and create the SimpleWorkerRequest.
var request = new Mock<HttpRequestBase>(MockBehavior.Strict);
var moqRequestContext = new Mock<RequestContext>(MockBehavior.Strict);
request.SetupGet<RequestContext>(r => r.RequestContext).Returns(moqRequestContext.Object);
var routeData = new RouteData();
routeData.Values.Add("key1", "value1");
moqRequestContext.Setup(r => r.RouteData).Returns(routeData);
request.SetupGet(x => x.ApplicationPath).Returns(PathProvider.GetAbsolutePath(""));
public interface IPathProvider
{
string GetAbsolutePath(string path);
}
public class PathProvider : IPathProvider
{
private readonly HttpServerUtilityBase _server;
public PathProvider(HttpServerUtilityBase server)
{
_server = server;
}
public string GetAbsolutePath(string path)
{
return _server.MapPath(path);
}
}
精彩评论