ASP.NET MVC End-to-End Integration Testing






4.50/5 (3 votes)
ASP.NET MVC application are highly testable when controllers are considered plain classes, but then you lose the integration with model validation, filters, method selectors, etc.
Introduction
This tip describes how the Xania.AspNet.Simulator
library helps with integration testing of MVC application, in a way that makes it enjoyable. No complex boilerplate code is needed anymore to test your controller action, while providing support for action filters, model validation, action selectors and more.
Background
I've been working on many different ASP.NET MVC applications and, despite all good initial intentions, most of these projects were not built with testability in mind. In my last project, I was explicitly tasked with improving code quality through refactoring code, in short, replacing static
classes with services, removing duplicate code block, splitting up methods into smaller pieces with less responsibility, etc.
I found myself in a situation where nothing is like it seems. The code base is nothing near "Self-documenting", even the 'Login
' action is much more than just simply logging in the user.
The solution to this was starting from the top, at the action level, where I had control of the request, response, headers, session, cookies, routing, filters, selectors, viewdata and modelstate and write tests at that level before starting refactoring the code. Unfortunately, ASP.NET MVC framework does not support an easy way to write tests for controller actions that support all these features by default. So, I started coding to bundle all in an easy to use package named Xania.AspNet.Simulator.
Using the Code
Take for example the following account controller implementation:
[Authorize]
public class AccountController : System.Web.Mvc.Controller
{
public AccountController()
{
}
//
// GET: /Account/Login
[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
[HttpPost]
[AllowAnonymous]
public ActionResult Login(LoginViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
var user = // find user
if (user != null)
{
return Redirect(returnUrl);
}
else
{
ModelState.AddModelError("", "Invalid username or password.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
//
// POST: /Account/LogOff
[HttpPost]
public ActionResult LogOff()
{
// sign out user
return RedirectToAction("Index", "Home");
}
}
Let's start writing integration tests.
- The first
Login
action is accessible by anonymous users and theViewBag.ReturnUrl
is set.[Test] public void GetLoginTest() { // arrange var action = new AccountController().Action(c => c.Login("[returnUrl]")); // act var result = action.Execute(); // assert Assert.IsInstanceOf<ViewResult>(result.ActionResult); Assert.AreEqual("[returnUrl]", result.ViewBag.ReturnUrl); }
- The second
Login
action is accessible by anonymous users with post method and requiresLoginViewModel
to be valid.[Test] public void InvalidPostLoginTest() { // arrange, when http method is GET var action = new AccountController().Action (c => c.Login(null), "GET" /* is default */); // assert Assert.IsNull(action); // not found } [Test] public void PostLoginTest() { // arrange var model = new LoginViewModel { UserName = "user1", Password = "passw1" }; var action = new AccountController().Action(c => c.Login(model), "POST"); // act var result = action.Execute(); // assert, assume user exists Assert.IsInstanceOf<RedirectResult>(result.ActionResult); Assert.AreEqual("[returnUrl]", result.ViewBag.ReturnUrl); }
- The
logoff
action is accessible by authorized users only withpost
method.[Test] public void PostLogOffTest() { // arrange var action = new AccountController().Action(c => c.LogOff(), "POST"); // act var result = action.Authenticate("user1", new string[0]).Execute(); // assert Assert.IsInstanceOf<RedirectToRouteResult>(result.ActionResult); } [Test] public void PostLogOffUnauthenticatedTest() { // arrange var action = new AccountController().Action(c => c.LogOff(), "POST"); // act, but don't authenticate var result = action.Execute(); // assert Assert.IsInstanceOf<HttpUnauthorizedResult>(result.ActionResult); }
Points of Interest
The code base is on github:
And a distribution can be found at NuGet with Id Xania.AspNet.Simulator
.