ASP.NET Core對(duì)Controller進(jìn)行單元測(cè)試的完整步驟
前言
單元測(cè)試對(duì)我們的代碼質(zhì)量非常重要。很多同學(xué)都會(huì)對(duì)業(yè)務(wù)邏輯或者工具方法寫(xiě)測(cè)試用例,但是往往忽略了對(duì)Controller層寫(xiě)單元測(cè)試。我所在的公司沒(méi)見(jiàn)過(guò)一個(gè)對(duì)Controller寫(xiě)過(guò)測(cè)試的。今天來(lái)演示下如果對(duì)Controller進(jìn)行單元測(cè)試。以下內(nèi)容默認(rèn)您對(duì)單元測(cè)試有所了解,比如如何mock一個(gè)接口。在這里多叨叨一句,面向接口的好處,除了能夠快速的替換實(shí)現(xiàn)類(其實(shí)大部分接口不會(huì)有多個(gè)實(shí)現(xiàn)),最大的好處就是可以進(jìn)行mock,可以進(jìn)行單元測(cè)試。
測(cè)試Action
下面的Action非常簡(jiǎn)單,非常常見(jiàn)的一種代碼。根據(jù)用戶id去獲取用戶信息然后展示出來(lái)。下面看看如何對(duì)這個(gè)Action進(jìn)行測(cè)試。
public class UserController : Controller { private readonly IUserService _userService; public UserController(IUserService userService) { _userService = userService; } public IActionResult UserInfo(string userId) { if (string.IsNullOrEmpty(userId)) { throw new ArgumentNullException(nameof(userId)); } var user = _userService.Get(userId); return View(user); } }
測(cè)試代碼:
[TestMethod()] public void UserInfoTest() { var userService = new Mock<IUserService>(); userService.Setup(s => s.Get(It.IsAny<string>())).Returns(new User()); var ctrl = new UserController(userService.Object); //對(duì)空參數(shù)進(jìn)行assert Assert.ThrowsException<ArgumentNullException>(() => { var result = ctrl.UserInfo(null); }); //對(duì)空參數(shù)進(jìn)行assert Assert.ThrowsException<ArgumentNullException>(() => { var result = ctrl.UserInfo(""); }); var result = ctrl.UserInfo("1"); Assert.IsNotNull(result); Assert.IsInstanceOfType(result, typeof(ViewResult)); }
我們對(duì)一個(gè)Action進(jìn)行測(cè)試主要的思路就是模擬各種入?yún)ⅲ箿y(cè)試代碼能夠到達(dá)所有的分支,并且Assert輸出是否為空,是否為指定的類型等。
對(duì)ViewModel進(jìn)行測(cè)試
我們編寫(xiě)Action的時(shí)候還會(huì)涉及ViewModel給視圖傳遞數(shù)據(jù),這部分也需要進(jìn)行測(cè)試。修改測(cè)試用例,加入對(duì)ViewModel的測(cè)試代碼:
[TestMethod()] public void UserInfoTest() { var userService = new Mock<IUserService>(); userService.Setup(s => s.Get(It.IsAny<string>())).Returns(new User() { Id = "x" }) ; var ctrl = new UserController(userService.Object); Assert.ThrowsException<ArgumentNullException>(() => { var result = ctrl.UserInfo(null); }); Assert.ThrowsException<ArgumentNullException>(() => { var result = ctrl.UserInfo(""); }); var result = ctrl.UserInfo("1"); Assert.IsNotNull(result); Assert.IsInstanceOfType(result, typeof(ViewResult)); //對(duì)viewModel進(jìn)行assert var vr = result as ViewResult; Assert.IsNotNull(vr.Model); Assert.IsInstanceOfType(vr.Model, typeof(User)); var user = vr.Model as User; Assert.AreEqual("x", user.Id); }
對(duì)ViewData進(jìn)行測(cè)試
我們編寫(xiě)Action的時(shí)候還會(huì)涉及ViewData給視圖傳遞數(shù)據(jù),這部分同樣需要測(cè)試。修改Action代碼,對(duì)ViewData進(jìn)行賦值:
public IActionResult UserInfo(string userId) { if (string.IsNullOrEmpty(userId)) { throw new ArgumentNullException(nameof(userId)); } var user = _userService.Get(userId); ViewData["title"] = "user_info"; return View(user); }
修改測(cè)試用例,加入對(duì)ViewData的測(cè)試代碼:
[TestMethod()] public void UserInfoTest() { var userService = new Mock<IUserService>(); userService.Setup(s => s.Get(It.IsAny<string>())).Returns(new User() { Id = "x" }) ; var ctrl = new UserController(userService.Object); Assert.ThrowsException<ArgumentNullException>(() => { var result = ctrl.UserInfo(null); }); Assert.ThrowsException<ArgumentNullException>(() => { var result = ctrl.UserInfo(""); }); var result = ctrl.UserInfo("1"); Assert.IsNotNull(result); Assert.IsInstanceOfType(result, typeof(ViewResult)); var vr = result as ViewResult; Assert.IsNotNull(vr.Model); Assert.IsInstanceOfType(vr.Model, typeof(User)); var user = vr.Model as User; Assert.AreEqual("x", user.Id); //對(duì)viewData進(jìn)行assert Assert.IsTrue(vr.ViewData.ContainsKey("title")); var title = vr.ViewData["title"]; Assert.AreEqual("user_info", title); }
對(duì)ViewBag進(jìn)行測(cè)試
因?yàn)閂iewBag事實(shí)上是ViewData的dynamic類型的包裝,所以Action代碼不用改,可以直接對(duì)ViewBag進(jìn)行測(cè)試:
[TestMethod()] public void UserInfoTest() { var userService = new Mock<IUserService>(); userService.Setup(s => s.Get(It.IsAny<string>())).Returns(new User() { Id = "x" }) ; var ctrl = new UserController(userService.Object); Assert.ThrowsException<ArgumentNullException>(() => { var result = ctrl.UserInfo(null); }); Assert.ThrowsException<ArgumentNullException>(() => { var result = ctrl.UserInfo(""); }); var result = ctrl.UserInfo("1"); Assert.IsNotNull(result); Assert.IsInstanceOfType(result, typeof(ViewResult)); var vr = result as ViewResult; Assert.IsNotNull(vr.Model); Assert.IsInstanceOfType(vr.Model, typeof(User)); var user = vr.Model as User; Assert.AreEqual("x", user.Id); Assert.IsTrue(vr.ViewData.ContainsKey("title")); var title = vr.ViewData["title"]; Assert.AreEqual("user_info", title); //對(duì)viewBag進(jìn)行assert string title1 = ctrl.ViewBag.title; Assert.AreEqual("user_info", title1); }
設(shè)置HttpContext
我們編寫(xiě)Action的時(shí)候很多時(shí)候需要調(diào)用基類里的HttpContext,比如獲取Request對(duì)象,獲取Path,獲取Headers等等,所以有的時(shí)候需要自己實(shí)例化HttpContext以進(jìn)行測(cè)試。
var ctrl = new AccountController(); ctrl.ControllerContext = new ControllerContext(); ctrl.ControllerContext.HttpContext = new DefaultHttpContext();
對(duì)HttpContext.SignInAsync進(jìn)行mock
我們使用ASP.NET Core框架進(jìn)行登錄認(rèn)證的時(shí)候,往往使用HttpContext.SignInAsync進(jìn)行認(rèn)證授權(quán),所以單元測(cè)試的時(shí)候也需要進(jìn)行mock。下面是一個(gè)典型的登錄Action,對(duì)密碼進(jìn)行認(rèn)證后調(diào)用SignInAsync在客戶端生成登錄憑證,否則跳到登錄失敗頁(yè)面。
public async Task<IActionResult> Login(string password) { if (password == "123") { var claims = new List<Claim> { new Claim("UserName","x") }; var authProperties = new AuthenticationProperties { }; var claimsIdentity = new ClaimsIdentity( claims, CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties); return Redirect("login_success"); } return Redirect("login_fail"); }
HttpContext.SignInAsync其實(shí)個(gè)時(shí)擴(kuò)展方法,SignInAsync其實(shí)最終是調(diào)用了IAuthenticationService里的SignInAsync方法。所以我們需要mock的就是IAuthenticationService接口,否者代碼走到HttpContext.SignInAsync會(huì)提示找不到IAuthenticationService的service。而IAuthenticationService本身是通過(guò)IServiceProvider注入到程序里的,所以同時(shí)需要mock接口IServiceProvider。
[TestMethod()] public async Task LoginTest() { var ctrl = new AccountController(); var authenticationService = new Mock<IAuthenticationService>(); var sp = new Mock<IServiceProvider>(); sp.Setup(s => s.GetService(typeof(IAuthenticationService))) .Returns(() => { return authenticationService.Object; }); ctrl.ControllerContext = new ControllerContext(); ctrl.ControllerContext.HttpContext = new DefaultHttpContext(); ctrl.ControllerContext.HttpContext.RequestServices = sp.Object; var result = await ctrl.Login("123"); Assert.IsNotNull(result); Assert.IsInstanceOfType(result, typeof(RedirectResult)); var rr = result as RedirectResult; Assert.AreEqual("login_success", rr.Url); result = await ctrl.Login("1"); Assert.IsNotNull(result); Assert.IsInstanceOfType(result, typeof(RedirectResult)); rr = result as RedirectResult; Assert.AreEqual("login_fail", rr.Url); }
對(duì)HttpContext.AuthenticateAsync進(jìn)行mock
HttpContext.AuthenticateAsync同樣比較常用。這個(gè)擴(kuò)展方法同樣是在IAuthenticationService里,所以測(cè)試代碼跟上面的SignInAsync類似,只是需要對(duì)AuthenticateAsync繼續(xù)mock返回值success or fail。
public async Task<IActionResult> Login() { if ((await HttpContext.AuthenticateAsync()).Succeeded) { return Redirect("/home"); } return Redirect("/login"); }
測(cè)試用例:
[TestMethod()] public async Task LoginTest1() { var authenticationService = new Mock<IAuthenticationService>(); //設(shè)置AuthenticateAsync為success authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>())) .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(new System.Security.Claims.ClaimsPrincipal(), ""))); var sp = new Mock<IServiceProvider>(); sp.Setup(s => s.GetService(typeof(IAuthenticationService))) .Returns(() => { return authenticationService.Object; }); var ctrl = new AccountController(); ctrl.ControllerContext = new ControllerContext(); ctrl.ControllerContext.HttpContext = new DefaultHttpContext(); ctrl.ControllerContext.HttpContext.RequestServices = sp.Object; var act = await ctrl.Login(); Assert.IsNotNull(act); Assert.IsInstanceOfType(act, typeof(RedirectResult)); var rd = act as RedirectResult; Assert.AreEqual("/home", rd.Url); //設(shè)置AuthenticateAsync為fail authenticationService.Setup(s => s.AuthenticateAsync(It.IsAny<HttpContext>(), It.IsAny<string>())) .ReturnsAsync(AuthenticateResult.Fail("")); act = await ctrl.Login(); Assert.IsNotNull(act); Assert.IsInstanceOfType(act, typeof(RedirectResult)); rd = act as RedirectResult; Assert.AreEqual("/login", rd.Url); }
Filter進(jìn)行測(cè)試
我們寫(xiě)Controller的時(shí)候往往需要配合很多Filter使用,所以Filter的測(cè)試也很重要。下面演示下如何對(duì)Fitler進(jìn)行測(cè)試。
public class MyFilter: ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { if (context.HttpContext.Request.Path.Value.Contains("/abc/")) { context.Result = new ContentResult() { Content = "拒絕訪問(wèn)" }; } base.OnActionExecuting(context); } }
對(duì)Filter的測(cè)試最主要的是模擬ActionExecutingContext參數(shù),以及其中的HttpContext等,然后對(duì)預(yù)期進(jìn)行Assert。
[TestMethod()] public void OnActionExecutingTest() { var filter = new MyFilter(); var actContext = new ActionContext(new DefaultHttpContext(),new RouteData(), new ActionDescriptor()); actContext.HttpContext.Request.Path = "/abc/123"; var listFilters = new List<IFilterMetadata>(); var argDict = new Dictionary<string, object>(); var actExContext = new ActionExecutingContext( actContext , listFilters , argDict , new AccountController() ); filter.OnActionExecuting(actExContext); Assert.IsNotNull(actExContext.Result); Assert.IsInstanceOfType(actExContext.Result, typeof(ContentResult)); var cr = actExContext.Result as ContentResult; Assert.AreEqual("拒絕訪問(wèn)", cr.Content); actContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); actContext.HttpContext.Request.Path = "/1/123"; listFilters = new List<IFilterMetadata>(); argDict = new Dictionary<string, object>(); actExContext = new ActionExecutingContext( actContext, listFilters, argDict, new AccountController() ); filter.OnActionExecuting(actExContext); Assert.IsNull(actExContext.Result); }
總結(jié)
到此這篇關(guān)于ASP.NET Core對(duì)Controller進(jìn)行單元測(cè)試的文章就介紹到這了,更多相關(guān)ASP.NET Core對(duì)Controller單元測(cè)試內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- .NET Core單元測(cè)試的兩種方法介紹
- ASP.NET?Core項(xiàng)目使用xUnit進(jìn)行單元測(cè)試
- 淺談.Net Core后端單元測(cè)試的實(shí)現(xiàn)
- xUnit 編寫(xiě) ASP.NET Core 單元測(cè)試的方法
- ASP.NET Core針對(duì)一個(gè)使用HttpClient對(duì)象的類編寫(xiě)單元測(cè)試詳解
- 詳解.Net單元測(cè)試方法
- ASP.NET Core中使用xUnit進(jìn)行單元測(cè)試
- .NET單元測(cè)試使用AutoFixture按需填充的幾種方式和最佳實(shí)踐記錄
相關(guān)文章
Javascript 直接調(diào)用服務(wù)器C#代碼 ASP.NET Ajax實(shí)例
近來(lái)總有一些朋友會(huì)問(wèn)到一些入門(mén)的問(wèn)題,把這些問(wèn)題整理一下,寫(xiě)出來(lái)。在以前的文章里,曾經(jīng)利用純JS編寫(xiě)過(guò)Ajax引擎,在真正開(kāi)發(fā)的時(shí)候,大家都不喜歡以這種低效率的方式開(kāi)發(fā),利用MS Ajax的集成的引擎,可以簡(jiǎn)單不少工作。2010-03-03.net core中的Authorization過(guò)濾器使用
這篇文章主要介紹了.net core中的Authorization過(guò)濾器使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11asp.net(vb.net)獲取真實(shí)IP的函數(shù)
asp.net(vb.net)獲取真實(shí)IP的函數(shù),需要的朋友可以參考下。2010-11-11.NET 刷新頁(yè)面防止表單二次提交的實(shí)現(xiàn)方法
頁(yè)面上按鈕是服務(wù)器控件,現(xiàn)在刷新頁(yè)面要防止按鈕事件重復(fù)執(zhí)行。這篇文章給大家?guī)?lái)了.net刷新頁(yè)面防止表單二次提交的實(shí)現(xiàn)方法,非常不錯(cuò),感興趣的朋友一起看看吧2016-09-09