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, typeof(User));
      var user = vr.Model as User;
      Assert.AreEqual("x", 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, 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);
      //对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 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);
    }

总结

到此这篇关于ASP.NET Core对Controller进行单元测试的文章就介绍到这了,更多相关ASP.NET Core对Controller单元测试内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • xUnit 编写 ASP.NET Core 单元测试的方法

    还记得 .NET Framework 的 ASP.NET WebForm 吗?那个年代如果要在 Web 层做单元测试简直就是灾难啊..NET Core 吸取教训,在设计上考虑到了可测试性,就连 ASP.NET Core 这种 Web 或 API 应用要做单元测试也是很方便的.其中面向接口和依赖注入在这方面起到了非常重要的作用. 本文就来手把手教你如何用 xUnit 对 ASP.NET Core 应用做单元测试..NET Core 常用的测试工具还有 NUnit 和 MSTest,我本人习惯用 x

  • ASP.NET Core MVC如何实现运行时动态定义Controller类型

    昨天有个朋友在微信上问我一个问题:他希望通过动态脚本的形式实现对ASP.NET Core MVC应用的扩展,比如在程序运行过程中上传一段C#脚本将其中定义的Controller类型注册到应用中,问我是否有好解决方案.我当时在外边,回复不太方便,所以只给他说了两个接口/类型:IActionDescriptorProvider和ApplicationPartManager.这是一个挺有意思的问题,所以回家后通过两种方案实现了这个需求.源代码从这里下载. 一.实现的效果 我们先来看看实现的效果.如下所

  • ASP.NET Core中使用xUnit进行单元测试

    单元测试的功能自从MVC的第一个版本诞生的时候,就是作为一个重要的卖点来介绍的,通常在拿MVC与webform比较的时候,单元测试就是必杀底牌,把webform碾压得一无是处. 单元测试的重要性不用多说了,有单元测试的做兜底的项目,好比给开发人员买了份保险,当然这个保险的质量取决于单元测试的质量,那些一路Mock的单元测试,看起来很美,但是什么都cover不到.目前工作中的一个老项目,有2万多个单元测试用例,其中不少是用心之作,真正落实到了业务逻辑,开发人员可以放心的去修改代码,当然一切都必须按

  • ASP.NET Core针对一个使用HttpClient对象的类编写单元测试详解

    介绍 几年前,微软引入了HttpClient类来替代HttpWebRequest来发送Web请求.这个新的类更易于使用,更加简洁,更具有异步性,且易于扩展. HttpClient类有一个可以接受HttpMessageHandler类对象的构造函数.HttpMessageHandler类对象可以接受一个请求(HttpRequestMessage), 并返回响应(HttpResponseMessage).它的功能完全取决于它的实现.默认情况下HttpClient使用的是HttpClientHandl

  • ASP.NET Core中的Controller使用示例

    ASP.NET CORE出现之前我们实现的Controller,MVC都继承自Controller基类,WebApi的话继承自ApiController.现在ASP.NET CORE把MVC跟WebApi合并了,已经不再区分MVC或者WebApi.ASP.NET CORE的Controller继承结构也发生了变化.我们看其他示例的时候会发现有些继承自Controller有些继承自ControllerBase.事实上ControllerBase是Controller的基类.也就是说如果你继承自Co

  • ASP.NET Core对Controller进行单元测试的完整步骤

    前言 单元测试对我们的代码质量非常重要.很多同学都会对业务逻辑或者工具方法写测试用例,但是往往忽略了对Controller层写单元测试.我所在的公司没见过一个对Controller写过测试的.今天来演示下如果对Controller进行单元测试.以下内容默认您对单元测试有所了解,比如如何mock一个接口.在这里多叨叨一句,面向接口的好处,除了能够快速的替换实现类(其实大部分接口不会有多个实现),最大的好处就是可以进行mock,可以进行单元测试. 测试Action 下面的Action非常简单,非常常

  • ASP.NET Core项目使用xUnit进行单元测试

    目录 一.前言 二.创建示例项目 1.UnitTest.Model 2.UnitTest.Data 3.UnitTest.IRepository 4.UnitTest.Repository 5.UnitTestDemo 6.TestDemo 三.编写单元测试 1.运行单元测试 2.调试单元测试 一.前言 在以前的.NET Framework项目中,我们也写过一些单元测试的项目,而在ASP.NET Core 这种Web或者API应用程序中要做单元测试是很方便的. 这篇文章主要讲解如何使用xUnit

  • ASP.NET Core Controller与IOC结合问题整理

    前言 看到标题可能大家会有所疑问Controller和IOC能有啥羁绊,但是我还是拒绝当一个标题党的.相信有很大一部分人已经知道了这么一个结论,默认情况下ASP.NET Core的Controller并不会托管到IOC容器中,注意关键字我说的是"默认",首先咱们不先说为什么,如果还有不知道这个结论的同学们可以自己验证一下,验证方式也很简单,大概可以通过以下几种方式. 验证Controller不在IOC中 首先,我们可以尝试在ServiceProvider中获取某个Controller实

  • Asp.Net Core利用xUnit进行主机级别的网络集成测试详解

    前言 在开发 Asp.Net Core 应用程序的过程中,我们常常需要对业务代码编写单元测试,这种方法既快速又有效,利用单元测试做代码覆盖测试,也是非常必要的事情:但是,但我们需要对系统进行集成测试的时候,需要启动服务主机,利用浏览器或者Postman 等网络工具对接口进行集成测试,这就非常的不方便,同时浪费了大量的时间在重复启动应用程序上:今天要介绍就是如何在不启动应用程序的情况下,对 Asp.Net Core WebApi 项目进行网络集成测试. 一.建立项目 1.1 首先我们建立两个项目,

  • ASP.NET Core中的Action的返回值类型实现

    在Asp.net Core之前所有的Action返回值都是ActionResult,Json(),File()等方法返回的都是ActionResult的子类.并且Core把MVC跟WebApi合并之后Action的返回值体系也有了很大的变化. ActionResult类 ActionResult类是最常用的返回值类型.基本沿用了之前Asp.net MVC的那套东西,使用它大部分情况都没问题.比如用它来返回视图,返回json,返回文件等等.如果是异步则使用Task. public class Te

  • Asp.net core中依赖注入的实现

    使用服务 在Asp.net core的Controller中,可以通过如下两种方式获取系统注入的服务: 构造函数 可以直接在构造函数中传入所依赖的服务,这是非常常见的DI注入方式.     public ValuesController(IConfiguration cfg)     {     } FromService参数 也可以直接在参数中通过FromServiceAttribute引入服务,这个在Controller中用起来非常方便,可以不用再构造函数中加一个变量以保存服务.     [

  • ASP.NET Core MVC 依赖注入View与Controller

    目录 一.ASP.NET Core MVC 之依赖注入 View 1.填充查找数据 2.重写服务 二. ASP.NET Core MVC 之依赖注入 Controller 1.构造函数注入 2.使用 FromServices 操作注入 3.在控制器中访问设置 一.ASP.NET Core MVC 之依赖注入 View ASP.NET Core 支持在试图中使用依赖注入.这将有助于提供视图专用的服务,比如本地化或者仅用于填充视图元素的数据.应尽量保持控制器和视图之间的关注点分离.视图所显示的大部分

随机推荐