Click here to Skip to main content
15,885,767 members
Articles / DevOps / Testing
Tip/Trick

ASP.NET MVC End-to-End Integration Testing

Rate me:
Please Sign up or sign in to vote.
4.50/5 (3 votes)
5 Dec 2014CPOL2 min read 34.7K   8   8
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:

C#
[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.

  1. The first Login action is accessible by anonymous users and the ViewBag.ReturnUrl is set.
    C#
    [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);
    }
  2. The second Login action is accessible by anonymous users with post method and requires LoginViewModel to be valid.
    C#
    [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);
    }
  3. The logoff action is accessible by authorized users only with post method.
    C#
    [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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Netherlands Netherlands
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionIs this a Controller mocking feature? Pin
mwpowellhtx2-Jan-17 12:27
mwpowellhtx2-Jan-17 12:27 
GeneralRe: Is this a Controller mocking feature? Pin
mwpowellhtx2-Jan-17 12:30
mwpowellhtx2-Jan-17 12:30 
To clarify, I am looking for true, end-to-end testing, including, but not limited to, sense as to whether filters are working. That is, if the filters are indeed working, then I should see a successful response, just as with an unfiltered server/controller.

However, from reading the published web docs, it seems this is only a controller action level verification, and not, in fact, end-to-end testing?

Thank you once again!
GeneralRe: Is this a Controller mocking feature? Pin
Ibrahim ben Salah27-Feb-17 22:26
Ibrahim ben Salah27-Feb-17 22:26 
AnswerRe: Is this a Controller mocking feature? Pin
Ibrahim ben Salah27-Feb-17 22:13
Ibrahim ben Salah27-Feb-17 22:13 
QuestionHow would I call an Action that has parameters; eg. new MyController().Action(c => c.Delete(1, 1)) Pin
Heistrouble4-Mar-15 9:30
Heistrouble4-Mar-15 9:30 
AnswerRe: How would I call an Action that has parameters; eg. new MyController().Action(c => c.Delete(1, 1)) Pin
Ibrahim ben Salah5-Mar-15 12:27
Ibrahim ben Salah5-Mar-15 12:27 
AnswerRe: How would I call an Action that has parameters; eg. new MyController().Action(c => c.Delete(1, 1)) Pin
Ibrahim ben Salah6-Mar-15 22:23
Ibrahim ben Salah6-Mar-15 22:23 
GeneralMy vote of 3 Pin
visu_19-Feb-15 13:53
visu_19-Feb-15 13:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.