使用HttpContext测试ASP.NET MVC控制器

36

我正在尝试编写一个单元测试来验证我的一个控制器是否正确返回了视图,但是这个控制器有一个基础控制器访问了HttpContext.Current.Session。每次我创建一个新的控制器实例时,它都会调用基础控制器构造函数,导致测试失败并在HttpContext.Current.Session上引发空指针异常。以下是代码:

public class BaseController : Controller
{       
    protected BaseController()
    {
       ViewData["UserID"] = HttpContext.Current.Session["UserID"];   
    }
}

public class IndexController : BaseController
{
    public ActionResult Index()
    {
        return View("Index.aspx");
    }
}

    [TestMethod]
    public void Retrieve_IndexTest()
    {
        // Arrange
        const string expectedViewName = "Index";

        IndexController controller = new IndexController();

        // Act
        var result = controller.Index() as ViewResult;

        // Assert
        Assert.IsNotNull(result, "Should have returned a ViewResult");
        Assert.AreEqual(expectedViewName, result.ViewName, "View name should have been {0}", expectedViewName);
    }

有什么建议可以使用Moq来模拟访问基控制器中的Session,以便测试继承控制器时能够运行?

5个回答

72

除非你使用Typemock或Moles,否则无法进行对象模拟

在ASP.NET MVC中,不应该使用HttpContext.Current。将基类更改为使用ControllerBase.ControllerContext - 它有一个 HttpContext 属性,可以公开可测试的 HttpContextBase 类。

这里是一个使用Moq设置Mock HttpContextBase的示例:

var httpCtxStub = new Mock<HttpContextBase>();

var controllerCtx = new ControllerContext();
controllerCtx.HttpContext = httpCtxStub.Object;

sut.ControllerContext = controllerCtx;

// Exercise and verify the sut

其中sut代表系统测试对象(SUT),即您希望测试的控制器。


请问您能指导我如何使用 Moles 进行模拟吗? - BritishDeveloper
毫无疑问这是正确的方法;微软为此特别添加了HttpContextBase。现在只要Cache和FormsAuthentication没有被sealed/static就好了…… - David Keaveny

5
如果您正在使用Typemock,您可以这样做:
Isolate.WhenCalled(()=>controller.HttpContext.Current.Session["UserID"])
.WillReturn("your id");

测试代码将如下所示:
[TestMethod]
public void Retrieve_IndexTest()
{
    // Arrange
    const string expectedViewName = "Index";

    IndexController controller = new IndexController();
    Isolate.WhenCalled(()=>controller.HttpContext.Current.Session["UserID"])
    .WillReturn("your id");
    // Act
    var result = controller.Index() as ViewResult;

    // Assert
    Assert.IsNotNull(result, "Should have returned a ViewResult");
    Assert.AreEqual(expectedViewName, result.ViewName, "View name should have been {0}", expectedViewName);
}

3

片段:

var request = new SimpleWorkerRequest("/dummy", @"c:\inetpub\wwwroot\dummy", "dummy.html", null, new StringWriter());
var context = new HttpContext(request);
SessionStateUtility.AddHttpSessionStateToContext(context, new TestSession());
HttpContext.Current = context;

实现TestSession()方法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.SessionState;

namespace m1k4.Framework.Test
{
    public class TestSession : IHttpSessionState
    {
        private Dictionary<string, object> state = new Dictionary<string, object>();

        #region IHttpSessionState Members

        public void Abandon()
        {
            throw new NotImplementedException();
        }

        public void Add(string name, object value)
        {
            this.state.Add(name, value);
        }

        public void Clear()
        {
            throw new NotImplementedException();
        }

        public int CodePage
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public System.Web.HttpCookieMode CookieMode
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public void CopyTo(Array array, int index)
        {
            throw new NotImplementedException();
        }

        public int Count
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public System.Collections.IEnumerator GetEnumerator()
        {
            throw new NotImplementedException();
        }

        public bool IsCookieless
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public bool IsNewSession
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public bool IsReadOnly
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public bool IsSynchronized
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public System.Collections.Specialized.NameObjectCollectionBase.KeysCollection Keys
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public int LCID
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public SessionStateMode Mode
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public void Remove(string name)
        {
            this.state.Remove(name);
        }

        public void RemoveAll()
        {
            this.state = new Dictionary<string, object>();
        }

        public void RemoveAt(int index)
        {
            throw new NotImplementedException();
        }

        public string SessionID
        {
            get
            {
                return "Test Session";
            }
        }

        public System.Web.HttpStaticObjectsCollection StaticObjects
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public object SyncRoot
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public int Timeout
        {
            get
            {
                return 10;
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public object this[int index]
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public object this[string name]
        {
            get
            {
                return this.state[name];
            }
            set
            {
                this.state[name] = value;
            }
        }

        #endregion
    }
}

1
如果您使用Rhino.Mocks,您可以简单地执行以下操作SessionStateUtility.AddHttpSessionStateToContext(httpContext, MockRepository.GenerateStub<IHttpSessionState>());而不是自己实现TestSession。 - Yoo Matsuo

2

在这种情况下,你应该使用ActionFilter而不是基类。

[UserIdBind]
public class IndexController : Controller
{
    public ActionResult Index()
    {
        return View("Index.aspx");
    }
}

如果我使用操作筛选器方法,那么我将不得不为每个操作都添加这个属性,而对于大约400个操作来说,这是不可行的。 - amurra
@user299592:不会的。你可以在类级别上应用它(根据示例),它将应用于该类中的每个操作。如果你认为这比在每个操作的每个测试中模拟出上下文要费力(其中很少或根本没有使用构造函数中设置的字段),那就没问题。 - pdr
你是正确的,我没有在类级别看到它,但是当实例化控制器对象时,动作过滤器仍然会被调用吗? - amurra
1
@user299592: 不会的。MVC框架会在每个动作前后调用它,但是你的单元测试根本不会调用它。这就是ActionFilters的美妙之处:你可以单独测试它们,并且只需要在测试一个方法时构建模拟/虚拟上下文,在你的情况下非常简单 - 一行代码,最多一个或两个测试。你的动作测试不需要关心这个,因为它已经被抽象出来了。 - pdr

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接