ASP.NET Core對Controller進行單元測試的完整步驟
前言
單元測試對我們的程式碼質量非常重要。很多同學都會對業務邏輯或者工具方法寫測試用例,但是往往忽略了對Controller層寫單元測試。我所在的公司沒見過一個對Controller寫過測試的。今天來演示下如果對Controller進行單元測試。以下內容預設您對單元測試有所瞭解,比如如何mock一個介面。在這裡多叨叨一句,面向介面的好處,除了能夠快速的替換實現類(其實大部分介面不會有多個實現),最大的好處就是可以進行mock,可以進行單元測試。
測試Action
下面的Action非常簡單,非常常見的一種程式碼。根據使用者id去獲取使用者資訊然後展示出來。下面看看如何對這個Action進行測試。
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); } }
測試程式碼:
[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); //對空引數進行assert Assert.ThrowsException<ArgumentNullException>(() => { var result = ctrl.UserInfo(null); }); //對空引數進行assert Assert.ThrowsException<ArgumentNullException>(() => { var result = ctrl.UserInfo(""); }); var result = ctrl.UserInfo("1"); Assert.IsNotNull(result); Assert.IsInstanceOfType(result,typeof(ViewResult)); }
我們對一個Action進行測試主要的思路就是模擬各種入參,使測試程式碼能夠到達所有的分支,並且Assert輸出是否為空,是否為指定的型別等。
對ViewModel進行測試
我們編寫Action的時候還會涉及ViewModel給檢視傳遞資料,這部分也需要進行測試。修改測試用例,加入對ViewModel的測試程式碼:
[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)); //對viewModel進行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); }
對ViewData進行測試
我們編寫Action的時候還會涉及ViewData給檢視傳遞資料,這部分同樣需要測試。修改Action程式碼,對ViewData進行賦值:
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); }
修改測試用例,加入對ViewData的測試程式碼:
[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,user.Id); //對viewData進行assert Assert.IsTrue(vr.ViewData.ContainsKey("title")); var title = vr.ViewData["title"]; Assert.AreEqual("user_info",title); }
對ViewBag進行測試
因為ViewBag事實上是ViewData的dynamic型別的包裝,所以Action程式碼不用改,可以直接對ViewBag進行測試:
[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,user.Id); Assert.IsTrue(vr.ViewData.ContainsKey("title")); var title = vr.ViewData["title"]; Assert.AreEqual("user_info",title); //對viewBag進行assert string title1 = ctrl.ViewBag.title; Assert.AreEqual("user_info",title1); }
設定HttpContext
我們編寫Action的時候很多時候需要呼叫基類裡的HttpContext,比如獲取Request物件,獲取Path,獲取Headers等等,所以有的時候需要自己例項化HttpContext以進行測試。
var ctrl = new AccountController(); ctrl.ControllerContext = new ControllerContext(); ctrl.ControllerContext.HttpContext = new DefaultHttpContext();
對HttpContext.SignInAsync進行mock
我們使用ASP.NET Core框架進行登入認證的時候,往往使用HttpContext.SignInAsync進行認證授權,所以單元測試的時候也需要進行mock。下面是一個典型的登入Action,對密碼進行認證後呼叫SignInAsync在客戶端生成登入憑證,否則跳到登入失敗頁面。
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其實個時擴充套件方法,SignInAsync其實最終是呼叫了IAuthenticationService裡的SignInAsync方法。所以我們需要mock的就是IAuthenticationService介面,否者程式碼走到HttpContext.SignInAsync會提示找不到IAuthenticationService的service。而IAuthenticationService本身是通過IServiceProvider注入到程式裡的,所以同時需要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); }
對HttpContext.AuthenticateAsync進行mock
HttpContext.AuthenticateAsync同樣比較常用。這個擴充套件方法同樣是在IAuthenticationService裡,所以測試程式碼跟上面的SignInAsync類似,只是需要對AuthenticateAsync繼續mock返回值success or fail。
public async Task<IActionResult> Login() { if ((await HttpContext.AuthenticateAsync()).Succeeded) { return Redirect("/home"); } return Redirect("/login"); }
測試用例:
[TestMethod()] public async Task LoginTest1() { var authenticationService = new Mock<IAuthenticationService>(); //設定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); //設定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進行測試
我們寫Controller的時候往往需要配合很多Filter使用,所以Filter的測試也很重要。下面演示下如何對Fitler進行測試。
public class MyFilter: ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { if (context.HttpContext.Request.Path.Value.Contains("/abc/")) { context.Result = new ContentResult() { Content = "拒絕訪問" }; } base.OnActionExecuting(context); } }
對Filter的測試最主要的是模擬ActionExecutingContext引數,以及其中的HttpContext等,然後對預期進行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("拒絕訪問",cr.Content); actContext = new ActionContext(new DefaultHttpContext(),new ActionDescriptor()); actContext.HttpContext.Request.Path = "/1/123"; listFilters = new List<IFilterMetadata>(); argDict = new Dictionary<string,object>(); actExContext = new ActionExecutingContext( actContext,new AccountController() ); filter.OnActionExecuting(actExContext); Assert.IsNull(actExContext.Result); }
總結
到此這篇關於ASP.NET Core對Controller進行單元測試的文章就介紹到這了,更多相關ASP.NET Core對Controller單元測試內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!