ASP.NET MVC編程——單元測試


1自動化測試基本概念

自動化測試分為:單元測試,集成測試,驗收測試。

單元測試

檢驗被測單元的功能,被測單元一般為低級別的組件,如一個類或類方法。

單元測試要滿足四個條件:自治的,可重復的,獨立的,快速的。

自治的是指:關注於驗證某個單一功能,例如只關注於類的某個方法的功能。

可重復的是指:無論何時允許同一段測試代碼都應該得到相同的結果。

獨立的是指:不依賴與其他任何系統或單元測試。

快速的是指:所有測試都應快速地完成,

 

集成測試

驗證兩個或多個組件之間的交互。

 

驗收測試

確保已構建的系統實現了既定的全部功能。

 

2准備進行單元測試

創建單元測試項目並執行測試應該依據一定的准則,運用一些技巧或工具,下面列舉了常用的技巧和工具。

命名規則

測試類應以被測試的單元命名,測試方法的名稱應能夠描述待驗證的行為。

 

使用特性

TestClassAttribute標識包含測試方法的類

TestMethodAttribute用於標識測試方法

TestInitializeAttribute標識在測試之前要運行的方法,從而分配並配置測試類中的所有測試所需的資源

ExpectedExceptionAttribute表示測試方法的執行過程中應引發異常用來判斷拋出的異常是否符合預期。

 

Arrange-Act-Assert模式

此模式又被稱為3A模式,Arrange,准備測試環境;Act,調用被測方法;Assert,斷言。

1:標准的3A模式,且只測試一個功能,即返回視圖對象是否為null,雖然待驗證的點有好幾個,但我們一次只驗證一個。

    [TestClass]
    public class HomeTest
    {
        [TestMethod]
        public void TestCacheExeActionResultNull()
        {
            //Arrange
            HomeController hc = new HomeController();

            //Act
            ViewResult vr = hc.CacheExe();

            //Assert
            Assert.IsNotNull(vr);
        }
     }

 

2:驗證參數為null時,是否會拋出預期的異常類型,即ArgumentNullException類型

    [TestClass]
    public class AccountTest
    {
        [TestMethod]
        [ExpectedException(typeof(ArgumentNullException))]
        public void TestLogin()
        {
            AccountController ac = new AccountController();

            ac.Login(null);
        }
    }

 

模擬依賴

為達到測試目的,使用假的組件模擬真實組件。有兩種方式模擬依賴:一種是創建模擬對象,另一種是使用框架。為能夠模擬依賴,使用存儲庫模式。

1:自定義模擬對象。

控制器:

    public class BookController : Controller
    {
        private IRepository repository;
        public BookController()
            : base()
        { }
        public BookController(IRepository repository)
        {
            this.repository = repository;
        }
        // GET: Book
        public ViewResult GetBook(int id)
        {
            var book = repository.GetBook(id);
            return View(book);
        }

        //其他代碼
    }

實現Repository

public class BookRepository:IRepository
{

        public Book GetBook(int id)
        {
            throw new NotImplementedException();
        }

        //其他代碼
}

定義IRepository接口

public interface IRepository
{
      Book GetBook(int id);

       //其他代碼
}

實體

public class Book
{
        public int Id { set; get; }
}

模擬對象

public class MocBookRepository : IRepository
{
        private Book bk;

        public MocBookRepository(Book bk)
        {
            this.bk = bk;
        }
        public Book GetBook(int id)
        {
            return bk;
        }
}

測試類

    [TestClass]
    public class BookTest
    {
        [TestMethod]
        public void TestGetBook()
        {
            Book exceptedBk = new Book
            {
                Id = 1
            };
            BookController bc = new BookController(new MocBookRepository(exceptedBk));

            ViewResult result = bc.GetBook(exceptedBk.Id);

            Assert.AreEqual(exceptedBk,result.Model);
        }
    }

 

2:使用模擬框架Moq

使用nuget下載Moq,截圖如下:

使用Moq:

        [TestMethod]
        public void TestGetBook()
        {
            Book exceptedBk = new Book
            {
                Id = 1
            };

            var mokRepository = new Moq.Mock<IRepository>();
            mokRepository.Setup(rep => rep.GetBook(exceptedBk.Id)).Returns(exceptedBk);

            BookController bc = new BookController(mokRepository.Object);
            var result = bc.GetBook(exceptedBk.Id);

            Assert.AreEqual(exceptedBk, result.Model);
        }

 

重構:去除重復代碼

例:

[TestClass]
public class HomeTest
{
        [TestMethod]
        public void TestCacheExeActionResultNull()
        {
            //Arrange
            HomeController hc = new HomeController();

            //Act
            ViewResult vr = hc.CacheExe();

            //Assert
            Assert.IsNotNull(vr);
        }

        [TestMethod]
        public void TestCacheExeActionValue()
        {
            //Arrange
            HomeController hc = new HomeController();

            //Act
            ViewResult vr = hc.CacheExe();

            //Assert
            Assert.AreEqual("緩存部分",vr.ViewBag.Sign);
        }
}

 

上面面的兩個測試方法含有共同的代碼,應將其提取,並作為測試所需的資源,先於測試方法執行。下面是改進后的代碼。

    [TestClass]
    public class HomeTest
    {
        private HomeController hc;
        private ViewResult vr;

        [TestInitialize]
        public void InitializeContext()
        {
            //Arrange
            hc = new HomeController();

            //Act
            vr = hc.CacheExe();
        }
        [TestMethod]
        public void TestCacheExeActionResultNull()
        {
            //Assert
            Assert.IsNotNull(vr);
        }

        [TestMethod]
        public void TestCacheExeActionValue()
        {
            //Assert
            Assert.AreEqual("緩存部分",vr.ViewBag.Sign);
        }
    }

 

3 測試ASP.NET MVC項目

3.1模擬HttpContext對象

        public void HttpContextForController(Controller controller)
        {
            var contextBaseMock = new Mock<HttpContextBase>();
            contextBaseMock.Setup(c=>c).Returns(new CustomHttpContext());
            controller.ControllerContext = new ControllerContext(new RequestContext(contextBaseMock.Object, new RouteData()), controller);
        }

        public class CustomHttpContext : HttpContextBase
        {
 
        }

 

3.2模擬Request對象

var contextBaseMock = new Mock<HttpContextBase>();
var method = "get";
contextBaseMock.Setup(c => c.Request.HttpMethod).Returns(method);
var mockHttpContext = contextBaseMock.Object;
或
var request = new Mock<HttpRequestBase>();
var headerValue = new NameValueCollection(){};//替換為具體實現
request.Setup(c =>c.Headers).Returns(headerValue);
var mockRequest = request.Object;

 

3.3模擬HttpResponse對象

var contextBaseMock = new Mock<HttpContextBase>();
contextBaseMock.Setup(c => c.Response.StatusCode).Returns(200);
var mockHttpContext = contextBaseMock.Object;
或
var response = new Mock<HttpResponseBase>();
var headerValue = new NameValueCollection(){};//替換為具體實現
response.Setup(c => c.Headers).Returns(headerValue);
var mockRequest = response.Object;

 

3.4模擬緩存對象

模擬Session對象

var contextBaseMock = new Mock<HttpContextBase>();
contextBaseMock.Setup(c => c.Session.Timeout).Returns(10);
var mockHttpContext = contextBaseMock.Object;

 

模擬Cache對象

var contextBaseMock = new Mock<HttpContextBase>();
contextBaseMock.Setup(c => c.Session.Timeout).Returns(10);
var mockHttpContext = contextBaseMock.Object;

 

3.5測試控制器

基本代碼如下,其中斷言部分會根據下面的測試項不同而不同

        public void TestGetBook()
        {
            Book exceptedBk = new Book
            {
                Id = 1
            };

            var mokRepository = new Moq.Mock<IRepository>();
            mokRepository.Setup(rep => rep.GetBook(exceptedBk.Id)).Returns(exceptedBk);

            BookController bc = new BookController(mokRepository.Object);
            var result = bc.GetBook(exceptedBk.Id);

//斷言部分 }

測試控制器操作的返回類型

Assert.IsInstanceOfType(result, typeof(ViewResult));

測試返回的視圖模型數據

Assert.AreEqual(exceptedBk, result.Model);
//
Assert.AreEqual(exceptedBk.Id,result.Model.Id);

測試重定向

控制器操作:

public RedirectResult Turn()
{
     return Redirect("~/home/index");
}

測試方法:

 

        [TestMethod]
        public void TestTurn()
        {
            BookController bc = new BookController();

            var result = bc.Turn();

            Assert.AreEqual("~/home/index", result.Url);
        }

 

3.6測試過濾器

雖然可能對控制器應用了過濾器,但單元測試調用控制器時是不會調用過濾器的;此外我們注冊的全局過濾器也不會被調用。要測試過濾器,就要模擬HTTP上下文、請求等。此外,建議將具體的驗證邏輯代碼封裝起來,這樣可以將其作為普通的類來測試。

例:

動作過濾器定義:

        public class CustomActionFilterAttribute : ActionFilterAttribute
        {
            public override void OnActionExecuted(ActionExecutedContext filterContext)
            {
                //具體實現
            }

            public override void OnActionExecuting(ActionExecutingContext filterContext)
            {
                //具體實現
            }
        }

權限過濾器定義:

public class CustomAuthorizeAttribute : AuthorizeAttribute
{
        private UserRole role;
        public CustomAuthorizeAttribute(UserRole role)
        {
            this.role = role;
        }
        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            //具體實現
        }
        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            //具體實現
        }
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            base.OnAuthorization(filterContext);
        }
}

public enum UserRole
{
        Org = 1,
        Vip = 2,
        Guest = 3
}

驗證動作過濾器CustomActionFilterAttribute

            //模擬Request
            var request = new Mock<HttpRequestBase>();
            request.SetupGet(r => r.HttpMethod).Returns("GET");
            request.SetupGet(r => r.Url).Returns(new Uri("http://basesit/controller/action"));

            //設置HttpContext,用模擬的Request設置HttpContext
            var httpContext = new Mock<HttpContextBase>();
            httpContext.SetupGet(c => c.Request).Returns(request.Object);

            //模擬ActionExecutedContext
            var actionExecutedContext = new Mock<ActionExecutedContext>();
            actionExecutedContext.SetupGet(c => c.HttpContext).Returns(httpContext.Object);

            //實例化待測試過濾器CustomActionFilterAttribute
            var customActionFilter = new CustomActionFilterAttribute();
            //調用執行方法,執行測試
            customActionFilter.OnActionExecuted(actionExecutedContext.Object);

            //模擬ActionExecutingContext
            var actionExecutingContext = new Mock<ActionExecutingContext>();
            actionExecutingContext.SetupGet(c => c.HttpContext).Returns(httpContext.Object);

            //調用執行方法,執行測試
            customActionFilter.OnActionExecuting(actionExecutingContext.Object);

驗證權限過濾器CustomAuthorizeAttribute

            //模擬Request
            var request = new Mock<HttpRequestBase>();
            request.SetupGet(r => r.HttpMethod).Returns("GET");
            request.SetupGet(r => r.Url).Returns(new Uri("http://basesit/controller/action"));

            //設置HttpContext,用模擬的Request設置HttpContext
            var httpContext = new Mock<HttpContextBase>();
            httpContext.SetupGet(c => c.Request).Returns(request.Object);
            //模擬AuthorizationContext
            var authorizationContext = new Mock<AuthorizationContext>();
            authorizationContext.SetupGet(c => c.HttpContext).Returns(httpContext.Object);

            //實例化待測試權限過濾器:CustomAuthorizeAttribute
            var authorizationFilter = new CustomAuthorizeAttribute(UserRole.Guest);
            //調用待測試方法
            authorizationFilter.OnAuthorization(authorizationContext.Object);

 

3.7測試視圖

視圖的測試主要通過實際運行,然后觀察瀏覽器渲染出來的結果,由於瀏覽器種類繁多,適配是也隨之變成了比較繁重的任務,依靠自動化測試不是最佳選擇,至少目前不是最佳選擇,但在此還是給出一個自動化測試的例子,這里使用WatiN測試套件,使用NuGet下載測試套件:

測試代碼

        [TestMethod]
        public void TestGetBookView()
        {
            string url = "http://localhost/MVCPointApp/Book/GetBook/1";
            using (var browser = new FireFox(url))
            {
                var bookDiv = browser.Div(Find.ByClass("pro_book"));
                var title = bookDiv.Element(Find.First()).Text;

                Assert.IsFalse(string.IsNullOrWhiteSpace(title));
                Assert.AreEqual("機器學習算法原理與編程實踐", title);
            }
        }

 

3.8測試路由

配置的路由模板為:

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );

測試被忽略的路由

        [TestMethod]
        public void TestIgnoreRoute()
        {
            var mock = new Mock<HttpContextBase>();
            mock.Setup(m => m.Request.AppRelativeCurrentExecutionFilePath).Returns("~/book.axd");
            var routes = new RouteCollection();

            var routeData = routes.GetRouteData(mock.Object);

            Assert.IsNull(routeData);
            Assert.IsInstanceOfType(routeData.RouteHandler,typeof(StopRoutingHandler));
        }

測試可匹配的路由

        [TestMethod]
        public void TestMatchedRoute()
        {
            var mock = new Mock<HttpContextBase>();
            mock.Setup(m => m.Request.AppRelativeCurrentExecutionFilePath).Returns("~/book/getbook/1");
            var routes = new RouteCollection();

            var routeData = routes.GetRouteData(mock.Object);

            Assert.IsNull(routeData);
            Assert.AreEqual("Book", routeData.Values["controller"]);
            Assert.AreEqual("GetBook", routeData.Values["action"]);
            Assert.AreEqual(UrlParameter.Optional, routeData.Values["id"]);

        }

 

4啟發:開發可測試的程序

即使對下面的概念沒有感覺,當實施一次單元測試以后就會深有體會。

 

基於接口編程

基於接口的編程,使得可以在測試的時候指定具體的類型,這樣解除了依賴,方便模擬組件。我們常見的相關概念是控制反轉(依賴注入)

 

使用IoC框架

使用成熟穩定的Ioc框架減少待測試的代碼量,減輕測試任務量。

 

存儲庫模式

使用存儲庫模式,將數據訪問邏輯與業務邏輯、控制器分離開來,測試控制器時可以借助此模式方便地模擬依賴,這樣將模塊合理地切分,實現測試只關注單一功能。

 

面向切面編程(APO

面向切面編程是面向對象編程的有力補充,降低業務處理中各個部分之間的耦合性,便於實施單元測試。

 

測試驅動開發(TDD

遵循“紅燈-綠燈-重構”的原則:從失敗的情況開始測試,然后編寫最少的代碼讓測試通過。為了能盡快地通過測試,編寫的最少量的代碼可能是未經過深思熟慮的,這種情況下就要重構。

 

參考:

1.Jess Chadwick/Todd Snyder/Hrusikesh Panda,徐雷/徐揚譯。ASP.NET MVC4 Web編程

2.Jon Galloway/Phil Haack/Brad Wilson/K. Scott Allen,孫遠帥/鄒權譯  ASP.NET MVC4 高級編程(第四版)

3.Dino Esposito著,潘麗臣譯,ASP.NET MVC5編程實戰

 

轉載與引用請注明出處。

時間倉促,水平有限,如有不當之處,歡迎指正。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM