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編程實戰
轉載與引用請注明出處。
時間倉促,水平有限,如有不當之處,歡迎指正。