[Unit Test] 使用 HttpContextBase 取代 HttpContext.Current,提高可測試性

開發 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);
}
手動建立 HttpRequest 其實還蠻麻煩,因為裡面有太多的檢查機制

依照行為建立相關物件:

把會用到 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);
}
每次只要有用到 HttpContext.Current 都需要建立另外一個類別,說真的還挺折騰人的。
要有抽象、介面或 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; }
}
程式碼裡面就不應該再使用 HttpContext.Current,而是使用 CurrentHttpContext。
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 我們能輕易的模擬假物件,不需要像上述方法再建立一個類別

結論:

HttpContextBase 提高了 Web 的可測試性,經過上述的演練,應該在專案裡面加上 HttpContextBase 屬性,避免直接使用 HttpContext.Current

 

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo