ASP. NET Core Complete steps for unit testing Controller

  • 2021-11-13 07:11:41
  • OfStack

Preface

Unit testing is very important to the quality of our code. Many students will write test cases for business logic or tool methods, but they often ignore writing unit tests for Controller layer. My company has never seen a test written for Controller. Today, let's demonstrate how to unit test Controller. You know something about unit testing by default, such as how to use mock1 interfaces. One more sentence here, the advantage of interface-oriented is that besides being able to quickly replace implementation classes (in fact, most interfaces will not have multiple implementations), the biggest advantage is that mock can be carried out and unit tests can be carried out.

Test Action

The following Action is a very simple and common code. According to the user id, get the user information and display it. Let's take a look at how to test this 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);
    }
   
  }

Test code:


 [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);
      // For a null parameter assert
      Assert.ThrowsException<ArgumentNullException>(() => {
        var result = ctrl.UserInfo(null);
      });
      // For a null parameter assert
      Assert.ThrowsException<ArgumentNullException>(() => {
        var result = ctrl.UserInfo("");
      });

      var result = ctrl.UserInfo("1");
      Assert.IsNotNull(result);
      Assert.IsInstanceOfType(result, typeof(ViewResult));
    }

The main idea of testing an Action is to simulate various input parameters, so that the test code can reach all branches, and whether the output of Assert is empty or of the specified type.

Test ViewModel

When we wrote Action, we also involved ViewModel passing data to views, which also needs to be tested. Modify the test case and add the test code for 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));
      // Right viewModel Go on 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);
    }

Test ViewData

When we wrote Action, we also involved ViewData passing data to views, which also needs to be tested. Modify the Action code to assign a value to 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);
    }
   

Modify the test case and add the test code for 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);
      // Right viewData Go on assert
      Assert.IsTrue(vr.ViewData.ContainsKey("title"));
      var title = vr.ViewData["title"];
      Assert.AreEqual("user_info", title);
    }

Test ViewBag

Because ViewBag is actually the dynamic type wrapper of ViewData, the Action code can be directly tested without changing:


   [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);
      // Right viewBag Go on assert
      string title1 = ctrl.ViewBag.title;
      Assert.AreEqual("user_info", title1);
    }

Setting HttpContext

When we write Action, we often need to call HttpContext in the base class, such as getting Request objects, getting Path, getting Headers, etc., so sometimes we need to instantiate HttpContext for testing.


  var ctrl = new AccountController();
  ctrl.ControllerContext = new ControllerContext();
  ctrl.ControllerContext.HttpContext = new DefaultHttpContext();

Perform mock on HttpContext. SignInAsync

When we use ASP. NET Core framework for login authentication, we often use HttpContext. SignInAsync for authentication authorization, so mock is also needed for unit testing. The following is a typical login Action. After authenticating the password, call SignInAsync to generate login credentials at the client, otherwise jump to the login failure page.


  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 is a real-time extension method, and SignInAsync actually calls SignInAsync method in IAuthenticationService. Therefore, what we need mock is IAuthenticationService interface. If the code goes to HttpContext. SignInAsync, it will prompt that service of IAuthenticationService cannot be found. IAuthenticationService itself is injected into the program through IServiceProvider, so mock interface IServiceProvider is needed at the same time.


  [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);
    }

Perform mock on HttpContext. AuthenticateAsync

HttpContext. AuthenticateAsync is also commonly used. This extension method is also in IAuthenticationService, so the test code is similar to SignInAsync above, except that you need to continue mock for AuthenticateAsync and return the value success or fail.


   public async Task<IActionResult> Login()
    {
      if ((await HttpContext.AuthenticateAsync()).Succeeded)
      {
        return Redirect("/home");
      }

      return Redirect("/login");
    }

Test cases:


 [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);
      // For a null parameter assert
      Assert.ThrowsException<ArgumentNullException>(() => {
        var result = ctrl.UserInfo(null);
      });
      // For a null parameter assert
      Assert.ThrowsException<ArgumentNullException>(() => {
        var result = ctrl.UserInfo("");
      });

      var result = ctrl.UserInfo("1");
      Assert.IsNotNull(result);
      Assert.IsInstanceOfType(result, typeof(ViewResult));
    }
0

Filter for testing

When we write Controller, we often need to use it with many Filter, so the test of Filter is also very important. The following shows how to test Fitler.


 [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);
      // For a null parameter assert
      Assert.ThrowsException<ArgumentNullException>(() => {
        var result = ctrl.UserInfo(null);
      });
      // For a null parameter assert
      Assert.ThrowsException<ArgumentNullException>(() => {
        var result = ctrl.UserInfo("");
      });

      var result = ctrl.UserInfo("1");
      Assert.IsNotNull(result);
      Assert.IsInstanceOfType(result, typeof(ViewResult));
    }
1

The test of Filter mainly simulates the parameters of ActionExecutingContext and HttpContext, and then carries out Assert for expectation.


 [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);
      // For a null parameter assert
      Assert.ThrowsException<ArgumentNullException>(() => {
        var result = ctrl.UserInfo(null);
      });
      // For a null parameter assert
      Assert.ThrowsException<ArgumentNullException>(() => {
        var result = ctrl.UserInfo("");
      });

      var result = ctrl.UserInfo("1");
      Assert.IsNotNull(result);
      Assert.IsInstanceOfType(result, typeof(ViewResult));
    }
2

Summarize


Related articles: