简体   繁体   中英

Unit test HttpContext.Current.Cache or other server-side methods in C#?

When creating a unit test for a class that uses the HttpContext.Current.Cache class , I get an error when using NUnit. The functionality is basic - check if an item is in the cache, and if not, create it and put it in:

if (HttpContext.Current.Cache["Some_Key"] == null) {
    myObject = new Object();
    HttpContext.Current.Cache.Insert("Some_Key", myObject);
}
else {
    myObject = HttpContext.Current.Cache.Get("Some_Key");
}

When calling this from a unit test, it fails with at NullReferenceException when encountering the first Cache line. In Java, I would use Cactus to test server-side code. Is there a similar tool I can use for C# code? This SO question mentions mock frameworks - is this the only way I can test these methods? Is there a similar tool to run tests for C#?

Also, I don't check if the Cache is null as I don't want to write code specifically for the unit test and assume it will always be valid when running on a server. Is this valid, or should I add null checks around the cache?

The way to do this is to avoid direct use of the HttpContext or other similar classes, and substitute them with mocks. After all, you're not trying to test that the HttpContext functions properly (that's microsoft's job), you're just trying to test that the methods got called when they should have.

Steps (In case you just want to know the technique without digging through loads of blogs):

  1. Create an interface which describes the methods you want to use in your caching (probably things like GetItem, SetItem, ExpireItem). Call it ICache or whatever you like

  2. Create a class which implements that interface, and passes methods through to the real HttpContext

  3. Create a class which implements the same interface, and just acts like a mock cache. It can use a Dictionary or something if you care about saving objects

  4. Change your original code so it doesn't use the HttpContext at all, and instead only ever uses an ICache. The code will then need to get an instance of the ICache - you can either pass an instance in your classes constructor (this is all that dependency injection really is), or stick it in some global variable.

  5. In your production app, set the ICache to be your real HttpContext-Backed-Cache, and in your unit tests, set the ICache to be the mock cache.

  6. Profit!

I agree with the others that using an interface would be the best option but sometimes it's just not feasible to change an existing system around. Here's some code that I just mashed together from one of my projects that should give you the results you're looking for. It's the farthest thing from pretty or a great solution but if you really can't change your code around then it should get the job done.

using System;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Web;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;

[TestFixture]
public class HttpContextCreation
{
    [Test]
    public void TestCache()
    {
        var context = CreateHttpContext("index.aspx", "http://tempuri.org/index.aspx", null);
        var result = RunInstanceMethod(Thread.CurrentThread, "GetIllogicalCallContext", new object[] { });
        SetPrivateInstanceFieldValue(result, "m_HostContext", context);

        Assert.That(HttpContext.Current.Cache["val"], Is.Null);

        HttpContext.Current.Cache["val"] = "testValue";
        Assert.That(HttpContext.Current.Cache["val"], Is.EqualTo("testValue"));
    }

    private static HttpContext CreateHttpContext(string fileName, string url, string queryString)
    {
        var sb = new StringBuilder();
        var sw = new StringWriter(sb);
        var hres = new HttpResponse(sw);
        var hreq = new HttpRequest(fileName, url, queryString);
        var httpc = new HttpContext(hreq, hres);
        return httpc;
    }

    private static object RunInstanceMethod(object source, string method, object[] objParams)
    {
        var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
        var type = source.GetType();
        var m = type.GetMethod(method, flags);
        if (m == null)
        {
            throw new ArgumentException(string.Format("There is no method '{0}' for type '{1}'.", method, type));
        }

        var objRet = m.Invoke(source, objParams);
        return objRet;
    }

    public static void SetPrivateInstanceFieldValue(object source, string memberName, object value)
    {
        var field = source.GetType().GetField(memberName, BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance);
        if (field == null)
        {
            throw new ArgumentException(string.Format("Could not find the private instance field '{0}'", memberName));
        }

        field.SetValue(source, value);
    }
}
HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null));

If you're using .NET 3.5, you can use System.Web.Abstractions in your application.

Justin Etheredge has a great post on how to mock HttpContext (which contains the cache class).

From Justin's example, I pass an HttpContextBase to my controllers using the HttpContextFactory.GetHttpContext. When mocking them, I just build a Mock to make calls to the cache object.

There is a newer approach to help deal specifically with Cache in unit tests.

I would recommend using Microsoft's new MemoryCache.Default approach. You will need to use .NET Framework 4.0 or later and include a reference to System.Runtime.Caching.

See article here --> http://msdn.microsoft.com/en-us/library/dd997357(v=vs.100).aspx

MemoryCache.Default works for both web and non-web applications. So the idea is you update your webapp to remove references to HttpContext.Current.Cache and replace them with references to MemoryCache.Default. Later, when you run decide to Unit Test these same methods, the cache object is still available and won't be null. (Because it is not reliant on an HttpContext.)

This way you don't even necessarily need to mock the cache component.

General consensus seems to be that driving anything HttpContext related from within a unit test is a total nightmare, and should be avoided if possible.

I think you're on the right path regarding mocking. I like RhinoMocks ( http://ayende.com/projects/rhino-mocks.aspx ).

I've read some good things about MoQ too ( http://code.google.com/p/moq ), although I've not tried it yet.

If you really want to write unit testable web UIs in C#, the way people seem to be heading is to use the MVC framework ( http://www.asp.net/mvc ) rather than WebForms...

You can use HttpContextBase class in System.Web.Abstractions.dll. It's a new dll in .NET 3.5.

You can find an example how to use in the link below.

http://vkreynin.wordpress.com/2009/03/23/stub-htttpcontext/

if you dont care about testing cache you can do below:

[TestInitialize]
    public void TestInit()
    {
      HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null));
    }

Also you can moq like below

var controllerContext = new Mock<ControllerContext>();
      controllerContext.SetupGet(p => p.HttpContext.Session["User"]).Returns(TestGetUser);
      controllerContext.SetupGet(p => p.HttpContext.Request.Url).Returns(new Uri("http://web1.ml.loc"));

This maybe up your street ... Phil Haack shows off, with the help of Rhino mocks, how to mock httpcontext in asp mvc but I'd imagine can be applied to webforms.

Clicky!!

Hope this helps.

An example for those using MVC 3 and MOQ:

My controller method has the following line:

model.Initialize(HttpContext.Cache[Constants.C_CustomerTitleList] 
as Dictionary<int, string>);

As such, any unit test will fail as I'm not setting up HttpContext.Cache.

In my unit test, I arrange as follows:

 HttpRuntime.Cache[Constants.C_CustomerTitleList] = new Dictionary<int, string>();

 var mockRequest = new Mock<HttpRequestBase>();
 mockRequest.SetupGet(m => m.Url).Returns(new Uri("http://localhost"));

 var context = new Mock<HttpContextBase>(MockBehavior.Strict);
 context.SetupGet(x => x.Request).Returns(mockRequest.Object);
 context.SetupGet(x => x.Cache).Returns(HttpRuntime.Cache);

 var controllerContext = new Mock<ControllerContext>();
 controllerContext.SetupGet(x => x.HttpContext).Returns(context.Object);

 customerController.ControllerContext = controllerContext.Object;

Could try...

 Isolate.WhenCalled(() => HttpContext.Current).ReturnRecursiveFake();
 var fakeSession = HttpContext.Current.Session;
 Isolate.WhenCalled(() => fakeSession.SessionID).WillReturn("1");

All of these programming questions ask for an interface based programming model, in which you implement the interface twice. One for the real code and one for the mockup.

Instantiation is then the next issue. There are several design patterns that can be used for that. See for example the famous GangOfFour Creational patterns ( GOF ) or the Dependency Injection patterns.

ASP.Net MVC is in fact using this interface-based approach and is therefore much more suitable for unit testing.

就像每个人都说的那样, HTTPContext存在一个问题,当前Typemock是唯一可以直接伪造它而无需任何包装或抽象的框架。

The cache object is difficult to mock because it's a sealed area of the .NET framework. I typically get around this by building a cache wrapper class that accepts a cache manager object. For testing, I use a mock cache manager; for production I use a cache manager that actually accesses HttpRuntime.Cache.

Basically, I abstract away the cache myself.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM