前言
單元測(cè)試對(duì)我們的代碼質(zhì)量非常重要。很多同學(xué)都會(huì)對(duì)業(yè)務(wù)邏輯或者工具方法寫測(cè)試用例,但是往往忽略了對(duì)Controller層寫單元測(cè)試。我所在的公司沒見過一個(gè)對(duì)Controller寫過測(cè)試的。今天來演示下如果對(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)單,非常常見的一種代碼。根據(jù)用戶id去獲取用戶信息然后展示出來。下面看看如何對(duì)這個(gè)Action進(jìn)行測(cè)試。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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è)試代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
[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è)試
我們編寫Action的時(shí)候還會(huì)涉及ViewModel給視圖傳遞數(shù)據(jù),這部分也需要進(jìn)行測(cè)試。修改測(cè)試用例,加入對(duì)ViewModel的測(cè)試代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
[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è)試
我們編寫Action的時(shí)候還會(huì)涉及ViewData給視圖傳遞數(shù)據(jù),這部分同樣需要測(cè)試。修改Action代碼,對(duì)ViewData進(jìn)行賦值:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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
我們編寫Action的時(shí)候很多時(shí)候需要調(diào)用基類里的HttpContext,比如獲取Request對(duì)象,獲取Path,獲取Headers等等,所以有的時(shí)候需要自己實(shí)例化HttpContext以進(jìn)行測(cè)試。
1
2
3
|
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è)面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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本身是通過IServiceProvider注入到程序里的,所以同時(shí)需要mock接口IServiceProvider。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
[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。
1
2
3
4
5
6
7
8
9
|
public async Task<IActionResult> Login() { if ((await HttpContext.AuthenticateAsync()).Succeeded) { return Redirect( "/home" ); } return Redirect( "/login" ); } |
測(cè)試用例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
[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è)試
我們寫Controller的時(shí)候往往需要配合很多Filter使用,所以Filter的測(cè)試也很重要。下面演示下如何對(duì)Fitler進(jìn)行測(cè)試。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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); } } |
對(duì)Filter的測(cè)試最主要的是模擬ActionExecutingContext參數(shù),以及其中的HttpContext等,然后對(duì)預(yù)期進(jìn)行Assert。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
[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 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)搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://www.cnblogs.com/kklldog/p/unit-test-core-controller.html