HttpContext doesn't like to be mocked!!
It's so easy to take a direct dependency on HttpContext and not even realize it. If you're in the code behind in Web Forms or in a controller action in MVC, it's just right there, tempting you to use it to access session variables, application security, etc.
But don't.
Some little known facts about HttpContext:
- HttpContext is the largest object ever created by humans.
- If you printed out the code for everything in HttpContext, the pages could be stacked end to end to wrap around the Earth's equator 7 times.
- Mocking HttpContext is like trying to calculate the last digit of pi. There is always a little more to it.
- Chuck Norris gave up trying to mock HttpContext. He was deep in HttpContext.Response and quit, curled into a ball on the floor, and started whimpering.
Sealed what? Object reference what?
Let's say you've got some MVC security stuff you're trying to work with in a controller action. You've got code like:
public class OrderController : Controller
{
[Authorize]
public ActionResult Process()
{
if (User.IsInRole("Admin"))
{
return View("SecretAdminStuff");
}
return View("NotAuthorized");
}
}
See that User.IsInRole()
code? That call is really to HttpContext.Current.User.IsInRole()
. You just took the bait and are tied to HttpContext
now.
So what? You have to use HttpContext to get that information, right? Sure, but you don't want to be tied directly to it or you've created untestable code.
Let's try a couple tests to verify the branching in this action method is returning the correct view based on the user's role. I'm using NUnit, Rhino Mocks, and the Should assembly below. Rhino Mocks has .Stub() and .Return() extension methods to set the expected return values for whether the user is or isn't in the Admin role.
[Test]
public void Should_return_NotAuthorized_view_when_not_Admin_user()
{
// Arrange
var mockContext = MockRepository.GenerateMock<HttpContext>();
mockContext.Stub(x => x.User.IsInRole("Admin")).Return(false);
var orderController = new OrderController();
// Act
var result = orderController.Process() as ViewResult;
// Assert
result.ViewName.ShouldEqual("NotAuthorized");
}
[Test]
public void Should_return_SecretAdminStuff_view_when_Admin_user()
{
// Arrange
var mockContext = MockRepository.GenerateMock<HttpContext>();
mockContext.Stub(x => x.User.IsInRole("Admin")).Return(true);
var orderController = new OrderController();
// Act
var result = orderController.Process() as ViewResult;
// Assert
result.ViewName.ShouldEqual("SecretAdminStuff");
}
As soon as we run the tests we see "System.NotSupportedException: Can't create mocks of sealed classes". Oh yeah, HttpContext is sealed. That won't work.
Hmmm. Aren't there some new classes in System.Web.Abstractions for just this kind of thing? Let's change HttpContext
var mockContext = MockRepository.GenerateMock<HttpContext>();
var mockContext = MockRepository.GenerateMock<HttpContextBase>();
and see if that helps.
The new problem is: "System.NullReferenceException: Object reference not set to an instance of an object." Mocking HttpContextBase
isn't helping because the test isn't passing the mock context into the controller. Every call in the action method to User.IsInRole()
will always call the real HttpContext
, which isn't created because the test is not running in the ASP.NET process.
You can get around this with Typemock Isolator, which goes beyond mocking and can intercept the next call to HttpContext.Current.User.IsInRole()
and swap out a result, but isn't there a simpler way to do this without buying another product or waiting for better abstract classes to work with?
A better way
We can't test something tied to HttpContext
like this. We need another approach. The one I favor is wrapping the calls we care about, delegating out to the untestable code in the wrapper class, then passing an interface to the wrapper class into the constructor of the class using it.
Here's the wrapping class I've created to access a few HttpContext
current user values and the interface for the wrapping class:
public class CurrentUser : ICurrentUser
{
public virtual string Name()
{
return HttpContext.Current.User.Identity.Name;
}
public bool IsLoggedIn()
{
return HttpContext.Current.User.Identity.IsAuthenticated;
}
public bool IsGuest()
{
return HttpContext.Current.User.IsInRole("Guest");
}
public bool IsAdmin()
{
return HttpContext.Current.User.IsInRole("Admin");
}
}
public interface ICurrentUser
{
string Name();
bool IsLoggedIn();
bool IsGuest();
bool IsAdmin();
}
Now I need to update the controller to inject this dependency into the constructor so it can be mocked, and use a private field set in the constructor for subsequent calls to _currentUser:
public class OrderController : Controller
{
private readonly ICurrentUser _currentUser;
public OrderController(ICurrentUser currentUser)
{
_currentUser = currentUser;
}
[Authorize]
public ActionResult Process()
{
if (_currentUser.IsAdmin())
{
return View("SecretAdminStuff");
}
return View("NotAuthorized");
}
}
Now we've got a testable action method. I mock the CurrentUser with MockRepository.GenerateMock<ICurrentUser>
, set its return value with the Rhino Mocks .Stub()
and .Return()
extension methods, and check for the expected results:
[Test]
public void Should_return_NotAuthorized_view_when_not_Admin_user()
{
// Arrange
var mockCurrentUser = MockRepository.GenerateMock<ICurrentUser>();
mockCurrentUser.Stub(x => x.IsAdmin()).Return(false);
var orderController = new OrderController(mockCurrentUser);
// Act
var result = orderController.Process() as ViewResult;
// Assert
result.ViewName.ShouldEqual("NotAuthorized");
}
[Test]
public void Should_return_SecretAdminStuff_view_when_Admin_user()
{
// Arrange
var mockCurrentUser = MockRepository.GenerateMock<ICurrentUser>();
mockCurrentUser.Stub(x => x.IsAdmin()).Return(true);
var orderController = new OrderController(mockCurrentUser);
// Act
var result = orderController.Process() as ViewResult;
// Assert
result.ViewName.ShouldEqual("SecretAdminStuff");
}
Hey, wait a sec
If you're thinking to yourself: I've just tested the code in the action method and not whether HttpContext
really works, you're exactly right. The tests should cover the branching in the controller action and are unit tests. They are not integration tests that check to see if HttpContext
is returning the right values.
I'm OK with that tradeoff. I know HttpContext
works. It's been around forever and it's in a System.<something>
namespace, so I shouldn't be testing it.
The real question is, do my login and roles and authorization settings work as expected in my application. That calls for integration testing, and the easiest way to do that is click through those screens and see if it's working. You can also automate this clicking around with Watin, or SpecFlow (which uses Watin internally to drive the browser).