How to mock application path when unit testing Web App

后端 未结 6 1272
囚心锁ツ
囚心锁ツ 2021-01-04 12:35

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 direc         


        
相关标签:
6条回答
  • 2021-01-04 12:56

    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

    0 讨论(0)
  • 2021-01-04 13:02

    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.

    0 讨论(0)
  • 2021-01-04 13:06
    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);
        }
    }
    
    0 讨论(0)
  • 2021-01-04 13:13

    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 strings myself to be safe). This is a bit annoying, but I think it is the best way to separate concerns in this situation.

    0 讨论(0)
  • 2021-01-04 13:14

    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

    0 讨论(0)
  • 2021-01-04 13:15

    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.

    0 讨论(0)
提交回复
热议问题