How to Successfully Mock ASP.NET HttpContext
For web developers, mocking an HttpContext is one of the hardest things to do. Today, I use two mocking frameworks to show you a number of ways to mock the HttpContext.
Since I've been using ASP.NET MVC, I've learned a lot about unit testing in the past 5 years (and a lot of that credit goes to Jon Kruger), but with all of that learning, I keep hearing about developers who have trouble with mocking HttpContext.
You always need to make sure that your code runs as independent as possible. Your environment can, and will, change. Therefore, you need to match the environment as much as possible to make your code work in an environment that isn't connected.
An environment that isn't connected? Whaaaaaat? What does that mean?
Your unit tests should run with no external resources. Zip. Zilch. Nada. None!
If you are connecting to a database, these are called integration tests. The database can be down for maintenance and your unit tests would fail.
Same for files on a hard drive. Integration test. The file is mandatory or the hard drive fills up and runs out of space. Unit test fails.
Same for accessing the Internet. External resource...integration test. Network is down. Dead unit test.
Unit tests should run on a machine with no connections to any external resources. Period.
Mocking Frameworks
So how do we test HttpContext without connecting to the Internet?
That is where our mocking frameworks enter the picture: Moq and RhinoMocks.
Both are important, but I think Moq is taking the lead based on the strong typing syntax instead of RhinoMocks' magic strings.
To show the similarities, I will be providing both Moq and Rhino Mocks examples.
Time to Mock the HttpContext
When we mock an object, what we are really saying is that we are creating dummy objects that mimic the behavior of real objects in our system. We want our dummy objects to act just like we're are using them in a production environment.
The baseline for mocking the HttpContext is pretty simple. We attach our TestInitialize attribute to our method and create our variables.
[TestClass] public class MockingHttpContextTest { private HttpContextBase rmContext; private HttpRequestBase rmRequest; private Mock<HttpContextBase> moqContext; private Mock<HttpRequestBase> moqRequest; [TestInitialize] public void SetupTests() { // Setup Rhino Mocks rmContext = MockRepository.GenerateMock<HttpContextBase>(); rmRequest = MockRepository.GenerateMock<HttpRequestBase>(); rmContext.Stub(x => x.Request).Return(rmRequest); // Setup Moq moqContext = new Mock<HttpContextBase>(); moqRequest = new Mock<HttpRequestBase>(); moqContext.Setup(x => x.Request).Returns(moqRequest.Object); }
Now we can start mocking our ASP.NET MVC routines very easily with a mocked-up HttpContext.
Routing
Since routing is deconstructed from the HttpContext, unit testing routes makes things a little more difficult, but we have a mocked HttpContext!
Routing is strictly used from the AppRelativeCurrentExecutionFilePath property (Phew! that's a mouthful) so we need to mock just that property.
#region Routing Tests [TestMethod] public void RhinoMocksRoutingTest() { // Arrange RouteCollection routes = new RouteCollection(); RouteConfig.RegisterRoutes(routes); rmRequest.Stub(e => e.AppRelativeCurrentExecutionFilePath).Return("~/Home/Index"); // Act RouteData routeData = routes.GetRouteData(rmContext); // Assert Assert.IsNotNull(routeData); Assert.AreEqual("Home",routeData.Values["controller"]); Assert.AreEqual("Index",routeData.Values["action"]); } [TestMethod] public void MoqRoutingTest() { // Arrange RouteCollection routes = new RouteCollection(); RouteConfig.RegisterRoutes(routes); moqRequest.Setup(e => e.AppRelativeCurrentExecutionFilePath).Returns("~/Home/Index"); // Act RouteData routeData = routes.GetRouteData(moqContext.Object); // Assert Assert.IsNotNull(routeData); Assert.AreEqual("Home", routeData.Values["controller"]); Assert.AreEqual("Index", routeData.Values["action"]); } #endregion
Sorry, but I included regions because we'll have more unit tests.
Forms and QueryStrings
While we could test to see if this data was submitted successfully through the controller, a better test is to make sure our request returns the right form data and query strings.
I've made another alteration to the SetupTests method at the top to accommodate our form/querystring values (in bold).
private NameValueCollection formValues; [TestInitialize] public void SetupTests() { // Setup Rhino Mocks rmContext = MockRepository.GenerateMock<HttpContextBase>(); rmRequest = MockRepository.GenerateMock<HttpRequestBase>(); rmContext.Stub(x => x.Request).Return(rmRequest); // Setup Moq moqContext = new Mock<HttpContextBase>(); moqRequest = new Mock<HttpRequestBase>(); moqContext.Setup(x => x.Request).Returns(moqRequest.Object); // Create a "fake" form formValues = new NameValueCollection { { "FirstName", "Jonathan" }, { "LastName", "Danylko" } }; }
While it may seem like a simple task to place the data and retrieve it back out, it confirms that we have our HttpContext and HttpRequest mocks objects setup properly.
#region Forms Tests [TestMethod] public void RhinoMocksFormsTest() { // Arrange rmRequest.Stub(r => r.Form).Return(formValues); // Act var forms = rmContext.Request.Form; // Assert Assert.IsNotNull(forms); Assert.AreEqual("Jonathan", forms["FirstName"]); Assert.AreEqual("Danylko", forms["LastName"]); } [TestMethod] public void MoqFormsTest() { // Arrange moqRequest.Setup(r => r.Form).Returns(formValues); // Act var forms = moqContext.Object.Request.Form; // Assert Assert.IsNotNull(forms); Assert.AreEqual("Jonathan", forms["FirstName"]); Assert.AreEqual("Danylko", forms["LastName"]); } #endregion #region QueryString Tests [TestMethod] public void RhinoMocksQueryStringTest() { // Arrange rmRequest.Stub(r => r.QueryString).Return(formValues); // Act var queryString = rmContext.Request.QueryString; // Assert Assert.IsNotNull(queryString); Assert.AreEqual("Jonathan", queryString["FirstName"]); Assert.AreEqual("Danylko", queryString["LastName"]); } [TestMethod] public void MoqQueryStringTest() { // Arrange moqRequest.Setup(r => r.QueryString).Returns(formValues); // Act var queryString = moqContext.Object.Request.QueryString; // Assert Assert.IsNotNull(queryString); Assert.AreEqual("Jonathan", queryString["FirstName"]); Assert.AreEqual("Danylko", queryString["LastName"]); } #endregion
BTW, this would also work with cookies.
Server Variables
Server Variables can be manipulated as well using a NameValueCollection.
Why would you want to test Server Variables?
If you want to test your code to display something if someone came from Google with a query search, you can display a message saying "Coming From Google? Thanks for visiting. Check out these links."
You would definitely want to Unit Test that functionality. Of course, you could use the Request.UrlReferrer, but I'm trying to prove a point that you could unit test Server Variables. ;-)
#region Server Variable Tests [TestMethod] public void RhinoMocksServerVariableTest() { // Arrange rmRequest.Stub(x => x.Url).Return(new Uri("http://localhost")); rmRequest.Stub(x => x.ServerVariables).Return(new NameValueCollection { {"SERVER_NAME", "localhost"}, {"SCRIPT_NAME", "localhost"}, {"SERVER_PORT", "80"}, {"REMOTE_ADDR", "127.0.0.1"}, {"REMOTE_HOST", "127.0.0.1"} }); // Act var variables = rmContext.Request.ServerVariables; // Assert Assert.IsNotNull(variables); Assert.AreEqual("localhost", variables["SERVER_NAME"]); } [TestMethod] public void MoqServerVariableTest() { // Arrange moqRequest.Setup(x => x.Url).Returns(new Uri("http://localhost")); moqRequest.Setup(x => x.ServerVariables).Returns(new NameValueCollection { {"SERVER_NAME", "localhost"}, {"SCRIPT_NAME", "localhost"}, {"SERVER_PORT", "80"}, {"REMOTE_ADDR", "127.0.0.1"}, {"REMOTE_HOST", "127.0.0.1"} }); // Act var variables = moqContext.Object.Request.ServerVariables; // Assert Assert.IsNotNull(variables); Assert.AreEqual("localhost", variables["SERVER_NAME"]); } #endregion
ControllerContext
Finally, we will create a mock ControllerContext. If you are doing any kind of controller unit tests, this is an absolute must.
For those new to unit testing ASP.NET MVC, you can't create a controller without a ControllerContext. It passes in the HttpContext (for the url information), the Routing Data (to compare with the url), and the actual controller for executing a method.
Once we have all three of those parameters, we can mock a ControllerContext.
#region ControllerContext [TestMethod] public void RhinoMocksControllerContextTest() { // Arrange var controller = new SubscribeController(); var context = new ControllerContext(rmContext, new RouteData(), controller); controller.ControllerContext = context; var parameters = new SubscribeParameter(); // Act var result = controller.SignUp(parameters) as ProcessResult<SubscribeParameter>; // Use ViewResult here for your results. This is a specific ActionResult I built. // Assert Assert.IsNotNull(result); } [TestMethod] public void MoqControllerContextTest() { // Arrange var controller = new SubscribeController(); controller.ControllerContext = new ControllerContext(moqContext.Object, new RouteData(), controller); var parameters = new SubscribeParameter(); // Act var result = controller.SignUp(parameters) as ProcessResult<SubscribeParameter>; // Use ViewResult here for your results. This is a specific ActionResult I built. // Assert Assert.IsNotNull(result); } #endregion
All we needed was a mocked HttpContext and a single controller to make sure everything worked for your unit test to pass.
The ProcessResult returned is a custom ActionResult I built. For the simple ViewResult, your unit test could check to see if you have a valid ViewModel returning from the ViewResult.
Conclusion
I hope this post simplifies the mocking of the HttpContext using either Moq or RhinoMocks. Once you see the pattern, it gets easier and easier to write unit tests.
The amount of unit tests for ASP.NET MVC can be staggering, but you can easily mock up any object for any test. The more you write, the better you'll become.
You just need to stick with it to write quality code.
Did I miss an MVC hook that I didn't cover in this post? Make me aware of it! Let me know in the comments below.