MVC中单元测试模拟HttpContext.Current.Session的问题
刚开始学习MVC1.0的时候,碰到一个单元测试问题。当时的场景在Controller里面使用了Session的数据,用户登录后将用户信息保存在Session中,大概是这样的场景吧,然后需要写一个单元测试模拟用户登录后的环境进行业务操作。在单元测试中,是不存在真正的Web环境的,即Session是不会真正被创建的,因此,必须模拟这个环境
首先是需要一个实现IHttpSessionState接口的类,然后实例化这个类 。
// 创建一个实现IHttpSessionState接口的类 using System; using System.Collections; using System.Collections.Specialized; using System.Globalization; using System.Text; using System.Threading; using System.Web; using System.Web.SessionState; namespace SxchangWebTest { public sealed class MySessionState : IHttpSessionState { const int MAX_TIMEOUT = 24 * 60; // Timeout cannot exceed 24 hours. string pId; ISessionStateItemCollection pSessionItems; HttpStaticObjectsCollection pStaticObjects; int pTimeout; bool pNewSession; HttpCookieMode pCookieMode; SessionStateMode pMode; bool pAbandon; bool pIsReadonly; public MySessionState(string id, ISessionStateItemCollection sessionItems, HttpStaticObjectsCollection staticObjects, int timeout, bool newSession, HttpCookieMode cookieMode, SessionStateMode mode, bool isReadonly) { pId = id; pSessionItems = sessionItems; pStaticObjects = staticObjects; pTimeout = timeout; pNewSession = newSession; pCookieMode = cookieMode; pMode = mode; pIsReadonly = isReadonly; } public int Timeout { get { return pTimeout; } set { if (value <= 0) throw new ArgumentException("Timeout value must be greater than zero."); if (value > MAX_TIMEOUT) throw new ArgumentException("Timout cannot be greater than " + MAX_TIMEOUT.ToString()); pTimeout = value; } } public string SessionID { get { return pId; } } public bool IsNewSession { get { return pNewSession; } } public SessionStateMode Mode { get { return pMode; } } public bool IsCookieless { get { return CookieMode == HttpCookieMode.UseUri; } } public HttpCookieMode CookieMode { get { return pCookieMode; } } // Abandon marks the session as abandoned. The IsAbandoned property is used by the // session state module to perform the abandon work during the ReleaseRequestState event. public void Abandon() { pAbandon = true; } public bool IsAbandoned { get { return pAbandon; } } // Session.LCID exists only to support legacy ASP compatibility. ASP.NET developers should use // Page.LCID instead. public int LCID { get { return Thread.CurrentThread.CurrentCulture.LCID; } set { Thread.CurrentThread.CurrentCulture = CultureInfo.ReadOnly(new CultureInfo(value)); } } // Session.CodePage exists only to support legacy ASP compatibility. ASP.NET developers should use // Response.ContentEncoding instead. public int CodePage { get { if (HttpContext.Current != null) return HttpContext.Current.Response.ContentEncoding.CodePage; else return Encoding.Default.CodePage; } set { if (HttpContext.Current != null) HttpContext.Current.Response.ContentEncoding = Encoding.GetEncoding(value); } } public HttpStaticObjectsCollection StaticObjects { get { return pStaticObjects; } } public object this[string name] { get { return pSessionItems[name]; } set { pSessionItems[name] = value; } } public object this[int index] { get { return pSessionItems[index]; } set { pSessionItems[index] = value; } } public void Add(string name, object value) { pSessionItems[name] = value; } public void Remove(string name) { pSessionItems.Remove(name); } public void RemoveAt(int index) { pSessionItems.RemoveAt(index); } public void Clear() { pSessionItems.Clear(); } public void RemoveAll() { Clear(); } public int Count { get { return pSessionItems.Count; } } public NameObjectCollectionBase.KeysCollection Keys { get { return pSessionItems.Keys; } } public IEnumerator GetEnumerator() { return pSessionItems.GetEnumerator(); } public void CopyTo(Array items, int index) { foreach (object o in items) items.SetValue(o, index++); } public object SyncRoot { get { return this; } } public bool IsReadOnly { get { return pIsReadonly; } } public bool IsSynchronized { get { return false; } } } } // 实例化MySessionState,并初创化 using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Reflection; using System.Text; using System.Web; using System.Web.Hosting; using System.Web.SessionState; namespace SxchangWebTest { public static class MockHttpContext { private const string ContextKeyAspSession = "AspSession"; private static HttpContext context = null; public static void Init() { MySessionState myState = new MySessionState(Guid.NewGuid().ToString("N"), new SessionStateItemCollection(), new HttpStaticObjectsCollection(), 5, true, HttpCookieMode.UseUri, SessionStateMode.InProc, false); TextWriter tw = new StringWriter(); // 这个地方是可以修改的,这是设置的Web路径的地方,但文件是可以不存在的 HttpWorkerRequest wr = new SimpleWorkerRequest("/webapp", "c://inetpub//wwwroot//webapp//", "default.aspx", "", tw); context = new HttpContext(wr); HttpSessionState state = Activator.CreateInstance( typeof(HttpSessionState), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.CreateInstance, null, new object[] { myState }, CultureInfo.CurrentCulture) as HttpSessionState; context.Items[ContextKeyAspSession] = state; HttpContext.Current = context; } public static HttpContext Context { get { return context; } } } }
然后是单元测试
// 简单的单元测试类 using NUnit.Framework; namespace SxchangWebTest { [TestFixture] public class MyHttpContextTest { [SetUp] public void Setup() { MockHttpContext.Init(); } [Test] public void MySessionTest() { //System.Web.HttpContext context = mock.Context; System.Web.HttpContext.Current.Session["aaa"] = "chang"; Assert.AreEqual("chang", (string)System.Web.HttpContext.Current.Session["aaa"]); Assert.IsNotNull(System.Web.HttpContext.Current.Server); Assert.IsNotNull(System.Web.HttpContext.Current.Request.FilePath); Assert.IsNotNull(System.Web.HttpContext.Current.Response); } [TearDown] public void Teardown() { } } }
这个是翻阅以前的一份笔记找出来的,年代久远,当时类似测试驱动开发,先在单元测试中实现逻辑,再写到正式的业务代码中,而项目经理通过验证单元测试通过率即可知道项目的基本情况(主流程要求全部需要单元测试);而且当进行业务改动时,相关单元测试会报错,然后开房人员就知道本次的修改会影响到其他的业务。现在感觉这样的开发形式比较少,维护单元测试的成本也比较大。