单元测试HttpContext.Current.Cache或C#中的其他服务器端方法?

时间:2009-01-28 22:59:06

标签: c# unit-testing nunit mocking

为使用HttpContext.Current.Cache class的类创建单元测试时,使用NUnit时出错。功能是基本的 - 检查项目是否在缓存中,如果没有,则创建它并将其放入:

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");
}

从单元测试中调用此功能时,遇到第一个NullReferenceException行时,它会在Cache处失败。在Java中,我会使用Cactus来测试服务器端代码。我可以使用类似的工具用于C#代码吗? This SO question提到了模拟框架 - 这是我测试这些方法的唯一方法吗?是否有类似的工具来运行C#测试?

另外,我不检查Cache是否为null,因为我不想专门为单元测试编写代码,并假设它在服务器上运行时始终有效。这是有效的,还是应该在缓存周围添加空检查?

14 个答案:

答案 0 :(得分:42)

这样做的方法是避免直接使用HttpContext或其他类似的类,并用模拟替换它们。毕竟,你不是试图测试HttpContext是否正常运行(这是微软的工作),你只是试图测试方法是否应该被调用。

步骤(如果您只想了解技术而不需要挖掘大量博客):

  1. 创建一个界面,描述您希望在缓存中使用的方法(可能是GetItem,SetItem,ExpireItem等)。称之为ICache或任何你喜欢的东西

  2. 创建一个实现该接口的类,并将方法传递给真正的HttpContext

  3. 创建一个实现相同接口的类,就像模拟缓存一样。如果您关心保存对象,它可以使用词典或其他东西

  4. 更改原始代码,使其根本不使用HttpContext,而只使用ICache。然后代码需要获取ICache的一个实例 - 你可以在类构造函数中传递一个实例(这就是依赖注入的真实情况),或者将它粘贴在某个全局变量中。

  5. 在您的生产应用中,将ICache设置为真正的HttpContext-Backed-Cache,并在单元测试中将ICache设置为模拟缓存。

  6. 利润!

答案 1 :(得分:29)

我同意其他人使用界面是最好的选择,但有时改变现有系统是不可行的。这里有一些代码,我只是从我的一个项目中混合在一起,它可以为您提供所需的结果。这是漂亮或出色的解决方案中最遥远的事情,但如果你真的无法改变你的代码那么它应该完成工作。

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);
    }
}

答案 2 :(得分:16)

HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null));

答案 3 :(得分:6)

如果您使用的是.NET 3.5,则可以在应用程序中使用System.Web.Abstractions。

Justin Etheredge在如何模拟HttpContext(包含缓存类)方面有一个很棒的post

从Justin的例子中,我使用HttpContextFactory.GetHttpContext将HttpContextBase传递给我的控制器。在模拟它们时,我只是构建一个Mock来调用缓存对象。

答案 4 :(得分:5)

有一种更新的方法可以帮助在单元测试中专门处理Cache。

我建议使用微软的新 MemoryCache.Default 方法。您将需要使用.NET Framework 4.0或更高版本,并包含对System.Runtime.Caching的引用。

请参阅此处的文章 - > http://msdn.microsoft.com/en-us/library/dd997357(v=vs.100).aspx

MemoryCache.Default适用于Web和非Web应用程序。因此,您的想法是更新您的webapp以删除对HttpContext.Current.Cache的引用,并将其替换为对MemoryCache.Default的引用。稍后,当您运行决定单元测试这些相同的方法时,缓存对象仍然可用,并且不会为空。 (因为它不依赖于HttpContext。)

这样你甚至不需要模拟缓存组件。

答案 5 :(得分:2)

普遍的共识似乎是在单元测试中驱动任何与HttpContext相关的内容都是一场噩梦,如果可能的话应该避免。

我认为你在嘲笑方面正走在正确的道路上。我喜欢RhinoMocks(http://ayende.com/projects/rhino-mocks.aspx)。

我也读过一些关于MoQ的好东西(http://code.google.com/p/moq),虽然我还没有尝试过。

如果你真的想用C#编写单元可测试的Web UI,人们似乎正在使用的方式是使用MVC框架(http://www.asp.net/mvc)而不是WebForms ......

答案 6 :(得分:2)

您可以在System.Web.Abstractions.dll中使用HttpContextBase类。这是.NET 3.5中的一个新dll。

您可以在下面的链接中找到如何使用的示例。

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

答案 7 :(得分:1)

这可能是你的街道...... Phil Haack在Rhino模拟的帮助下展示了如何在asp mvc中模拟httpcontext,但我想可以应用于webforms。

Clicky!!

希望这有帮助。

答案 8 :(得分:1)

如果您不关心测试缓存,可以执行以下操作:

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

你也可以像下面那样进行搜索

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"));

答案 9 :(得分:0)

所有这些编程问题都要求基于接口的编程模型,您可以在其中实现两次接口。一个用于真实代码,一个用于模型。

实例化是下一个问题。有几种设计模式可用于此。例如,参见着名的GangOfFour Creational模式(GOF)或依赖注入模式。

ASP.Net MVC实际上使用这种基于接口的方法,因此更适合于单元测试。

答案 10 :(得分:0)

正如大家在这里所说的,HTTPContext存在问题,目前Typemock是唯一可以直接伪造它而没有任何包装或抽象的框架。

答案 11 :(得分:0)

缓存对象很难模拟,因为它是.NET框架的密封区域。我通常通过构建一个接受缓存管理器对象的缓存包装类来解决这个问题。为了测试,我使用模拟缓存管理器;对于生产我使用实际访问HttpRuntime.Cache的缓存管理器。

基本上,我自己抽象出缓存。

答案 12 :(得分:0)

使用MVC 3和MOQ的例子:

我的控制器方法有以下几行:

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

因此,任何单元测试都会失败,因为我没有设置HttpContext.Cache。

在我的单元测试中,我安排如下:

 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;

答案 13 :(得分:0)

可以试试......

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