開發 Web 或多或少會使用到 HttpContext.Current,為了專注測主要邏輯,會隔離 HttpContext.Current,不要因為它而導致測試無法運行,我在這裡列出了一些隔離技巧
前言:
HttpContext.Current 需要透過 Page.Request 來建立(建立網站),HttpContext.Current 的成員幾乎都是唯讀屬性,很難對它直接修改,也就是無法使用 Mock Framework,但仍可手動透過 HttpRequest 建立,會辛苦一些,比較沒有那麼好用
.NET 3.5 之後引進了 HttpContextBase,這是一個抽象類別,IIS 可透過 HttpContextWrapper 建立 HttpContextBase,我們也可以建立假的 HttpContextBase 餵給 API,用 Mock Framework 建立 HttpContextBase 就容易許多了。
開發環境:
- VS 2015 Update 1
- Mock Framework:NSubstitute
本文開始:
這裡列出了我知道的隔離技巧:
範例專案位置:
https://dotblogsamples.codeplex.com/SourceControl/latest#Simple.MockHttpContext/
手動建立 HttpRequest:
建立假的 HttpContext,程式碼如下:
public static HttpContext CreateHttpContext()
{
var request = new HttpRequest("", "http://google.com", "");
var response = new HttpResponse(new StringWriter());
HttpContext context = new HttpContext(request, response);
return context;
}
public static HttpContext SetIdentity(this HttpContext httpContext, string name, bool isAuthenticated = true)
{
httpContext.User = new GenericPrincipal(new GenericIdentity(name), new string[0]);
return httpContext;
}
被測目標,他是一個 Web Service,裡面會用到 HttpContext.Current ,當驗證沒通過會返回 "No authentication" ,程式碼如下:
[WebMethod]
public string HelloWorld()
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
return "Hello, " + HttpContext.Current.User.Identity.Name;
}
return "No authentication";
}
測試程式碼,則是直接建立一個假的 HttpContext.Current 物件 ,程式碼如下:
[TestMethod]
public void TestMethod1()
{
var expected = "Hello, yao";
WebService1 ws = new WebService1();
HttpContext.Current = FakeHttpContextManager.CreateHttpContext().SetIdentity("yao");
var actual = ws.HelloWorld();
Assert.AreEqual(expected, actual);
}
依照行為建立相關物件:
把會用到 HttpContext.Current 方法移到成另外一個物件 ,程式碼如下:
public interface ICurrentUser
{
string GetName();
bool IsAuthenticated();
}
public class CurrentUser : ICurrentUser
{
public string GetName()
{
return HttpContext.Current.User.Identity.Name;
}
public bool IsAuthenticated()
{
return HttpContext.Current.User.Identity.IsAuthenticated;
}
}
讓服務依賴 ICurrentUser 而不是 HttpContext.Current ,程式碼如下:
public ICurrentUser CurrentUser { get; set; }
[WebMethod]
public string HelloWorld2()
{
if (this.CurrentUser.IsAuthenticated())
{
return "Hello, " + this.CurrentUser.GetName();
}
return "No authentication";
}
測試程式碼,則是用 Mock Framework 模擬假的 ICurrentUser ,程式碼如下:
[TestMethod]
public void TestMethod2()
{
var expected = "Hello, yao";
WebService1 ws = new WebService1();
var mock = Substitute.For<ICurrentUser>();
mock.IsAuthenticated().Returns(true);
mock.GetName().Returns("yao");
ws.CurrentUser = mock;
var actual = ws.HelloWorld2();
Assert.AreEqual(expected, actual);
}
要有抽象、介面或 virtual method, Mock Framework 才能模擬行為。
使用 Mock Framework 建立 HttpContextBase:
首先,在類別裡定義一個回傳 HttpContextBase 的屬性,開放給外部注入,由這個屬性決定是否該使用 new HttpContextWrapper(HttpContext.Current) ,也就是IIS,還是接受外部的 HttpContextBase ,程式碼如下:
private HttpContextBase _currentHttpContext;
public HttpContextBase CurrentHttpContext
{
get
{
if (this._currentHttpContext != null)
{
return _currentHttpContext;
}
return HttpContextFactory.GetHttpContext();
}
set { _currentHttpContext = value; }
}
CurrentHttpContext 能輕易的切換 IIS Page.Request 和 Mock 物件
集中管理HttpContextWrapper
public static class HttpContextFactory
{
[ThreadStatic]
private static HttpContextBase s_mockHttpContext;
public static void SetHttpContext(HttpContextBase httpContextBase)
{
s_mockHttpContext = httpContextBase;
}
public static void ResetHttpContext()
{
s_mockHttpContext = null;
}
public static HttpContextBase GetHttpContext()
{
if (s_mockHttpContext != null)
{
return s_mockHttpContext;
}
if (HttpContext.Current != null)
{
return new HttpContextWrapper(HttpContext.Current);
}
return null;
}
}
使用 NSubstitute(Mock Framework),來模擬 HttpContextBase,如下程式碼:
public static HttpContextBase CreateHttpContextBase()
{
var context = Substitute.For<HttpContextBase>();
var request = Substitute.For<HttpRequestBase>();
var response = Substitute.For<HttpResponseBase>();
var sessionState = Substitute.For<HttpSessionStateBase>();
var serverUtility = Substitute.For<HttpServerUtilityBase>();
context.Request.Returns(request);
context.Response.Returns(response);
context.Session.Returns(sessionState);
context.Server.Returns(serverUtility);
return context;
}
public static HttpContextBase SetIdentity(this HttpContextBase httpContextBase, string name, bool isAuthenticated = true)
{
var principal = Substitute.For<IPrincipal>();
var identity = Substitute.For<IIdentity>();
principal.Identity.Returns(identity);
httpContextBase.User.Returns(principal);
identity.Name.Returns(name);
identity.IsAuthenticated.Returns(isAuthenticated);
return httpContextBase;
}
被測目標,也是一個 Web Service,裡面會用到 CurrentHttpContext,當驗證沒通過會返回 "No authentication",程式碼如下,
[WebMethod]
public string HelloWorld3()
{
if (this.CurrentHttpContext.User.Identity.IsAuthenticated)
{
return "Hello, " + this.CurrentHttpContext.User.Identity.Name;
}
return "No authentication";
}
測試程式碼,由外部注入假的 HttpContextBase 模擬登入者已經通過驗證 ,程式碼如下:
[TestMethod]
public void TestMethod3()
{
var expected = "Hello, yao";
WebService1 ws = new WebService1();
var httpContext = FakeHttpContextManager.CreateHttpContextBase()
.SetIdentity("yao")
;
ws.CurrentHttpContext = httpContext;
var actual = ws.HelloWorld3();
Assert.AreEqual(expected, actual);
}
結論:
HttpContextBase 提高了 Web 的可測試性,經過上述的演練,應該在專案裡面加上 HttpContextBase 屬性,避免直接使用 HttpContext.Current
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET