Integration Testing Your ASP.NET MVC Application

Let’s start with a quick recap:

  • Unit tests test individual software components. They supply mocks or fake versions of dependencies (such as a database) so that the unit test doesn’t rely on any external code and any failures can be pinpointed exactly. Unit testing is central to Test Driven Development – the entire TDD process is driven by unit testing.

  • Integration tests test your entire software stack working together. These tests don’t mock or fake anything (they use the real database, and real network connections) and are good at spotting if your unit-tested components aren’t working together as you expected. In general, it’s best to put most of your effort into building a solid suite of unit tests, and then adding a few integration tests for each major feature so you can detect any catastrophic incompatibilities or configuration errors before your customers do.

ASP.NET MVC famously has great support for unit testing. That’s well covered elsewhere on the web, so I won’t write more about it here. But what about integration testing?

These tools host a real web browser and automate a series of navigation steps, clicking on buttons and letting you write assertions about what should appear on the screen. Browser automation is fully capable of testing your JavaScript code just as easily as your business logic. However, there are also some drawbacks to browser automation:

  • Browser automation test scripts are very fragile. As soon as you alter the HTML in your views, the tests may start failing and sometimes all need to be re-recorded. Many developers are happy with browser automation at first, but after a few months are sick of re-recording the tests or maintaining thousands of lines of scripts, and give up on it.

  • Browser automation test scripts rely on parsing the generated HTML, which loses all the strongly-typed goodness of your MVC model objects.

Introducing MvcIntegrationTestFramework

What if you could write NUnit tests (or xUnit or whatever you prefer) that directly submit a URL, query string, cookies, request headers, etc., into your application – without needing the app to be hosted in any web server but still running in the real (non-mocked) ASP.NET runtime – and get back the response text to make assertions about? In fact, what if you also got back the ActionResult (e.g., a ViewResult) that was executed, a reference to any unhandled exception it threw, and could also write assertions about cookie values or the contents of Session? What if you could string together a series of test requests to simulate a user’s browsing session, checking that a whole feature area works from end to end?
Well, my friend, all this can be yours with MvcIntegrationTestFramework

A Simple Integration Test Example
Consider the following action method:

public ActionResult Index()
{
ViewData["Message"] = "Welcome to ASP.NET MVC!";
return View();
}
You can write an integration for this using MvcIntegrationTestFramework as follows:
[Test]
public void Root_Url_Renders_Index_View()
{
appHost.SimulateBrowsingSession(browsingSession => {
// Request the root URL
RequestResult result = browsingSession.ProcessRequest("/");

// You can make assertions about the ActionResult...
var viewResult = (ViewResult) result.ActionExecutedContext.Result;
Assert.AreEqual("Index", viewResult.ViewName);
Assert.AreEqual("Welcome to ASP.NET MVC!", viewResult.ViewData["Message"]);
// ... or you can make assertions about the rendered HTML
Assert.IsTrue(result.ResponseText.Contains("<!DOCTYPE html"));
});
}
This is pretty similar to a typical unit test for that action method, except that you also get access
to the finished rendered HTML, in case that interests you. Unlike a unit test, this integration test 
goes through the entire request-processing pipeline, so it tests your routing configuration, your 
controller factory, any dependencies your controller has, and even your view template.

[Test]
public void LogInProcess()
{
string securedActionUrl = "/home/SecretAction";

appHost.SimulateBrowsingSession(browsingSession => {
// First try to request a secured page without being logged in
RequestResult initialRequestResult = browsingSession.
        ProcessRequest(securedActionUrl);
string loginRedirectUrl = initialRequestResult.Response.RedirectLocation;
Assert.IsTrue(loginRedirectUrl.StartsWith
       ("/Account/LogOn"), "Didn't redirect to logon page");

// Now follow redirection to logon page
string loginFormResponseText = browsingSession.ProcessRequest(loginRedirectUrl).ResponseText;
string suppliedAntiForgeryToken = MvcUtils.ExtractAntiForgeryToken(loginFormResponseText);

// Now post the login form, including the verification token
RequestResult loginResult = browsingSession.ProcessRequest(loginRedirectUrl,
        HttpVerbs.Post, new NameValueCollection
{
{ "username", "steve" },
{ "password", "secret" },
{ "__RequestVerificationToken", suppliedAntiForgeryToken }
});
string afterLoginRedirectUrl = loginResult.Response.RedirectLocation;
Assert.AreEqual(securedActionUrl, afterLoginRedirectUrl, "Didn't redirect back to SecretAction");

// Check that we can now follow the redirection back to the protected action, and are let in
RequestResult afterLoginResult = browsingSession.ProcessRequest(securedActionUrl);
Assert.AreEqual("Hello, you're logged in as steve", afterLoginResult.ResponseText);
});
}

In this test, multiple requests are made as part of the same browsing session. The integration testing framework will preserve cookies (and therefore session state) from one request to the next, so you can test interaction with things like Forms Authentication and ASP.NET MVC’s AntiForgeryToken helpers.

This has advantages over browser automation in that (in my opinion) it’s easier to write this test – it’s concise, you don’t need to learn a browser scripting language, and you don’t need to rerecord the script if you restructure the HTML. You can make assertions at the HTML level, or you can make assertions at the ActionResult level, with fully strongly-typed access to any ViewData or Model values that were being supplied.

On the flip side, browser automation is still the only way to test your JavaScript.

Integration Testing Your Own ASP.NET MVC Application

To add these kinds of tests to your own ASP.NET MVC application, create a new class library project called MyApp.IntegrationTests or similar. Add a reference to MvcIntegrationTestFramework.dll, which you can get by downloading the demo project and compiling it, and also add a reference to your chosen unit testing framework (e.g., NUnit or xUnit).

Next, if you’re using NUnit, create a basic test fixture class as follows:

using MvcIntegrationTestFramework.Hosting;
using MvcIntegrationTestFramework.Browsing;

[TestFixture]
public class MyIntegrationTests
{
// Amend this path to point to whatever folder contains your ASP.NET MVC application
// (i.e., the folder that contains your app's main web.config file)
private static readonly string mvcAppPath = Path.GetFullPath(
    AppDomain.CurrentDomain.BaseDirectory + "\\..\\..\\..\\MyMvcApplication");
private readonly AppHost appHost = new AppHost(mvcAppPath);
}


Also – and seriously you can’t skip this step – you must add a post-build step so that each time you compile, Visual Studio will copy the assemblies from your integration test project’s \bin folder into your main ASP.NET MVC application’s \bin folder. I’ll explain why this is necessary later.


image

The test fixture you just created tells MvcIntegrationTestFramework to find your ASP.NET MVC application at the specified disk location, and to host it (without using IIS or any other web server). The resulting AppHost object then provides an API for issuing requests to your application.


[TestFixture]
public class MyIntegrationTests{    // Leave rest as before     
[Test]    public void HomeIndex_DemoTests(){        
appHost.SimulateBrowsingSession(browsingSession => {            
RequestResult result = browsingSession.ProcessRequest("home/index");             
// Routing config should match "home" controller            
Assert.AreEqual("home", result.ActionExecutedContext.RouteData.Values["controller"]);             
// Should have rendered the "index" view            
ActionResult actionResult = result.ActionExecutedContext.Result;            
Assert.IsInstanceOf(typeof(ViewResult), actionResult);            
Assert.AreEqual("index", ((ViewResult)actionResult).ViewName);             
// App should not have had an unhandled exception            
Assert.IsNull(result.ResultExecutedContext.Exception);  
});    }}

Conclusion

Personally, I find this approach to integration testing more productive than browser automation in most cases, because the tests are fast to write and are less prone to break because of trivial HTML changes. I wouldn’t want to have thousands of integration tests, though – for one thing, they’d take ages to run – but end-to-end testing for key scenarios like logging in, registering, going through shopping carts, etc., proves pretty convincingly that the major functional areas of your app are working overall, leaving unit tests to prove the fine detail of each component.

If you have better solution, just tell me !

0 comments: