雖然身為開發老兵,最近才開始認真練習單元測試,不小心學到新技巧還會高興半天,嗯,那是一種,說不出來,但,確實,真的,又存在的感覺,是彷彿在魁北克無邊無際的,沙漠,之中的冰原般,的寂寞向日葵,專屬的小確幸(抱歉! 誤啟假文青模式)。今天就來分享一則幼幼班經驗 -- 當程式用到HttpRequest,要如何寫單元測試?

假設我們有個RequestParser類別,提供Parse(HttpRequestBase request)方法,由HttpRequestBase中取出指定Cookie、客戶端IP及UserAgent。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
 
namespace WebHelper
{
    public class ClientInfo
    {
        public string SrcCookie;
        public string ClientIp;
        public string UserAgent;
    }
 
    public class RequestParser
    {
        public static ClientInfo Parse(HttpRequestBase request)
        {
            return new ClientInfo()
            {
                SrcCookie = (request.Cookies["src"] ?? 
                             new HttpCookie("src", string.Empty)).Value,
                ClientIp = request.UserHostAddress,
                UserAgent = request.UserAgent
            };
        }
    }
}

講到這裡,應該已經有WebForm專修班同學舉手發問: "為什麼是用HttpRequestBase,不是HttpRequest嗎?"

故事是這樣的: HttpRequest誕生於.NET 1.1時代,屬於System.Web命名空間,可透過Page.Request及UserControl.Request存取,具唯讀性質。意思是你只能從Page或UserControl取得HttpRequest物件,無法自己建立或修改,這大大限制了它的應用彈性,難以進行單元測試,除非丟到網站,你很難生出一個HttpRequest物件執行測試案例。

有鑑於此,.NET 3.5 SP1加入了HttpRequestBase,具備HttpRequest所有屬性,但差別在於它是抽象類別,開發者可以繼承它寫出衍生類別加入客製邏輯。原本依賴HttpRequest的程式只需改用HttpRequestBase,程式碼毋須修改(因HttpRequstBase的介面與HttpRequest完全相同),這允許我們自行建立及操控HttpRequestBase參數進行測試及應用。而在IIS環境執行時,可使用HttpRequestWrapper將HttpRequest轉為HttpRequestBase,傳給改用HttpRequestBase型別的方法。像在ASP.NET MVC,Controller裡已不見HttpRequest蹤跡,全面改用HttpRequestBase。

寫個WebForm測試RequestParser:

using System;
using System.Web;
 
namespace Web
{
    public partial class Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            var client = WebHelper.RequestParser.Parse(
                new HttpRequestWrapper(this.Request));
            Response.Write("<li>Client IP=" + client.ClientIp);
            Response.Write("<li>Cookie=" + client.SrcCookie);
            Response.Write("<li>UserAgent=" + client.UserAgent);
            Response.End();
 
        }
    }
}

執行無誤!

把場景切到單元測試,在純Class Library中,要怎麼生出一個HttpRequestBase呢?

繼承HttpRequestBase寫一個MyHttpRequest是種做法,但我們的測試只用到Cookies、UserHostAddress、UserAgent三個屬性,為此寫一顆HttpRequestBase衍生類別實作數十個屬性方法形同"為了拔一根牛毛養一頭牛",人生苦短,嗯湯啊嗯湯。在測試世界裡,通常會用Mock、Stub及Fake技巧解決問題。

爬文得知目前較多人用的.NET Mock元件有Rhino Mocks及Moq,該用哪一個呢? 就使用"拿香跟著拜理論"決定吧!

RhinoMocks 最後更新於 2012/3/18,下載次數26萬次

Moq 最後更新於 2014/2/21,下載次數145萬次

二者相比,猶如德國 7: 1 踢垮巴西,用膝蓋也知道該怎麼選。更重要的一點是,Moq API大量使用Lambda,令LINQ成癮的我心醉,就用Moq吧!

Moq的使用方法很簡單,使用new Mock<HttpRequestBase>()可建立HttpRequestBase的Mock物件,接著以三個SetupGet()設定UserHostAddress、Cookies及UserAgent屬性的讀取行為。SetupGet()使用x => x.SomeProperty的Lambda語法指定要模擬的屬性。而如下圖所示,Moq巧妙地透過泛型宣告讓輸入Lambda時也有Intellisense支援,想打錯字都難(按個讚!)。每個SetupGet()後接上Returns()決定傳回結果。就這樣,我們模擬出一個只有HostUserAddress、Cookies、UserAgent三個屬性有效的陽春HttpRequestBase物件。

測試時,Mock<HttpRequestBase>.Object可當成HttpRequestBase使用,執行結果也如同預期。

藉由Mock物件,我們可以自由捏塑出所需參數型別並指定其行為,不需太多周邊環境配合,就能完成單元測試囉~ 關於Moq的更多說明,可參考官方文件


Comments

# by Alex Lee

等等.... 說好的Assert 咧?

# by Jeffrey

to Alex, 範例中的單元測試還沒寫完,Assert部分就給同學們當成作業囉~ (丟下粉筆逃出教室)

# by Billy

推薦一本書 Pro ASP.net MVC5 在詳細教授MVC5 的同時,使用Ninject 作DI Tools,用Visual Studio Unit Testing, 並使用Moq 作為mocking framework 可以把相關知識一並學會,內裡亦有講述HttpRequestBase, HttpContextBase, HttpResponseBase 作Unit Testing 以下是節錄的一段: UNIT TEST: TESTING INCOMING URLS http://goo.gl/6AKZZ1

# by Jeffrey

to Billy, 感謝補充。

Post a comment