Skip to content

Instantly share code, notes, and snippets.

@johnnyreilly
Last active October 29, 2019 13:43
Show Gist options
  • Star 27 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save johnnyreilly/4959924 to your computer and use it in GitHub Desktop.
Save johnnyreilly/4959924 to your computer and use it in GitHub Desktop.
What you need to unit test MVC controllers using MOQ.
using System.Web.Mvc;
namespace DemoApp.Areas.Demo
{
public class DemoAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "DemoArea";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"DemoArea_default",
"Demo/{oneTypeOfId}/{anotherTypeOfId}/{controller}/{action}/{id}",
new { oneTypeOfId = 0, anotherTypeOfId = 0, action = "Index", id = UrlParameter.Optional }
);
}
}
}
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
namespace DemoApp.Areas.Demo.Controllers
{
public class DemoController : System.Web.Mvc.Controller
{
//....
public JsonResult Edit(AnObject anObject)
{
//Indicate to the client we have saved and pass back the redirect URL
return Json(new {
Saved = true,
RedirectUrl = Url.Action("Details", anObject.AnotherTypeOfId)
});
}
//....
}
}
using Moq;
using System;
using System.Collections.Specialized;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace UnitTest.TestUtilities
{
/// <summary>
/// This class of MVC Mock helpers is originally based on Scott Hanselman's 2008 post:
/// http://www.hanselman.com/blog/ASPNETMVCSessionAtMix08TDDAndMvcMockHelpers.aspx
///
/// This has been updated and tweaked to work with MVC 3 / 4 projects (it hasn't been tested with MVC
/// 1 / 2 but may work there) and also based my use cases
/// </summary>
public static class MvcMockHelpers
{
#region Mock HttpContext factories
public static HttpContextBase MockHttpContext()
{
var context = new Mock<HttpContextBase>();
var request = new Mock<HttpRequestBase>();
var response = new Mock<HttpResponseBase>();
var session = new Mock<HttpSessionStateBase>();
var server = new Mock<HttpServerUtilityBase>();
request.Setup(r => r.AppRelativeCurrentExecutionFilePath).Returns("/");
request.Setup(r => r.ApplicationPath).Returns("/");
response.Setup(s => s.ApplyAppPathModifier(It.IsAny<string>())).Returns<string>(s => s);
response.SetupProperty(res => res.StatusCode, (int)System.Net.HttpStatusCode.OK);
context.Setup(h => h.Request).Returns(request.Object);
context.Setup(h => h.Response).Returns(response.Object);
context.Setup(ctx => ctx.Request).Returns(request.Object);
context.Setup(ctx => ctx.Response).Returns(response.Object);
context.Setup(ctx => ctx.Session).Returns(session.Object);
context.Setup(ctx => ctx.Server).Returns(server.Object);
return context.Object;
}
public static HttpContextBase MockHttpContext(string url)
{
var context = MockHttpContext();
context.Request.SetupRequestUrl(url);
return context;
}
#endregion
#region Extension methods
public static void SetMockControllerContext(this Controller controller,
HttpContextBase httpContext = null,
RouteData routeData = null,
RouteCollection routes = null)
{
//If values not passed then initialise
routeData = routeData ?? new RouteData();
routes = routes ?? RouteTable.Routes;
httpContext = httpContext ?? MockHttpContext();
var requestContext = new RequestContext(httpContext, routeData);
var context = new ControllerContext(requestContext, controller);
//Modify controller
controller.Url = new UrlHelper(requestContext, routes);
controller.ControllerContext = context;
}
public static void SetHttpMethodResult(this HttpRequestBase request, string httpMethod)
{
Mock.Get(request).Setup(req => req.HttpMethod).Returns(httpMethod);
}
public static void SetupRequestUrl(this HttpRequestBase request, string url)
{
if (url == null)
throw new ArgumentNullException("url");
if (!url.StartsWith("~/"))
throw new ArgumentException("Sorry, we expect a virtual url starting with \"~/\".");
var mock = Mock.Get(request);
mock.Setup(req => req.QueryString).Returns(GetQueryStringParameters(url));
mock.Setup(req => req.AppRelativeCurrentExecutionFilePath).Returns(GetUrlFileName(url));
mock.Setup(req => req.PathInfo).Returns(string.Empty);
}
/// <summary>
/// Facilitates unit testing of anonymouse types - taken from here:
/// http://stackoverflow.com/a/5012105/761388
/// </summary>
public static object GetReflectedProperty(this object obj, string propertyName)
{
obj.ThrowIfNull("obj");
propertyName.ThrowIfNull("propertyName");
var property = obj.GetType().GetProperty(propertyName);
if (property == null)
return null;
return property.GetValue(obj, null);
}
public static T ThrowIfNull<T>(this T value, string variableName) where T : class
{
if (value == null)
throw new NullReferenceException(
string.Format("Value is Null: {0}", variableName));
return value;
}
#endregion
#region Private
static string GetUrlFileName(string url)
{
return (url.Contains("?"))
? url.Substring(0, url.IndexOf("?"))
: url;
}
static NameValueCollection GetQueryStringParameters(string url)
{
if (url.Contains("?"))
{
var parameters = new NameValueCollection();
var parts = url.Split("?".ToCharArray());
var keys = parts[1].Split("&".ToCharArray());
foreach (var key in keys)
{
var part = key.Split("=".ToCharArray());
parameters.Add(part[0], part[1]);
}
return parameters;
}
return null;
}
#endregion
}
}
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace UnitTest.Areas.Demo.Controllers
{
[TestClass]
public class UnitTestingAnAreaUsingUrlHelper
{
private DemoController _controller;
[TestInitialize]
public void InitializeTest()
{
_controller = new DemoController();
}
[TestMethod]
public void Edit_updates_the_object_and_returns_a_JsonResult_containing_the_redirect_URL()
{
// Arrange
int anotherTypeOfId = 5332;
//Register the area as well as standard routes
RouteTable.Routes.Clear();
var areaRegistration = new DemoAreaRegistration();
var areaRegistrationContext = new AreaRegistrationContext(
areaRegistration.AreaName, RouteTable.Routes);
areaRegistration.RegisterArea(areaRegistrationContext);
RouteConfig.RegisterRoutes(RouteTable.Routes);
//Initialise the controller and setup the context so MVC can pick up the relevant route data
var httpContext = MvcMockHelpers.MockHttpContext(
"~/Demo/77969/" + anotherTypeOfId + "/Company/Edit");
var routeData = RouteTable.Routes.GetRouteData(httpContext);
_controller.SetMockControllerContext(
httpContext, routeData, RouteTable.Routes);
// Act
var result = _controller.Edit(
new AnObject{
WithAProperty = "Something",
AnotherTypeOfId = anotherTypeOfId });
// Assert
Assert.AreEqual("DemoArea", areaRegistration.AreaName);
Assert.IsInstanceOfType(result, typeof(JsonResult));
Assert.IsNotNull(result.Data,
"There should be some data for the JsonResult");
Assert.AreEqual(true,
result.Data.GetReflectedProperty("Saved"));
Assert.AreEqual("/Demo/77969/" + anotherTypeOfId + "/Company/Details",
result.Data.GetReflectedProperty("RedirectUrl"));
}
}
}
@ptsurbeleu
Copy link

Thanks for putting this altogether! Very handy, usable and useful!
Just a minor thing - seems like in MvcMockHelpers.cs, lines #35-39 are duplicates (unless I'm missing something).

Appreciate for sharing it!

@barrychesterman
Copy link

Thanks, I found this really handy!

I used this but updated MvcMockHelpers.cs to use a System.Uri object in SetupRequestUrl() instead of a relative url string so that I can populate HttpContextBase.Request.Url, I pass a Uri object in instead of the relative url allowing access to the Request.Url properties in the controller such as Request.Url.Scheme or Request.Url.Authority etc.

MvcMockHelpers.cs

public static void SetupRequestUrl(this HttpRequestBase request, Uri uri)
{
            if (uri == null)
                throw new ArgumentNullException("url");

            var mock = Mock.Get(request);

            //removed exception and replaced it by using local path prefixed with ~
            var localPath = "~" + uri.LocalPath; 

            mock.Setup(req => req.QueryString).Returns(GetQueryStringParameters(localPath));
            mock.Setup(req => req.AppRelativeCurrentExecutionFilePath).Returns(GetUrlFileName(localPath));
            mock.Setup(req => req.PathInfo).Returns(string.Empty);
            mock.Setup(req => req.Path).Returns(uri.LocalPath);

            //setup the uri object for the request context
            mock.Setup(req => req.Url).Returns(uri);
}

The calling functions such as MockHttpContext() also needed updating to pass the uri through.

Then my unit test setup all I do is create a uri object and pass it through;

[TestInitialize]
public void setup()
{
            ..
            var uri = new Uri("http://mydummyhost.local/urlpath/);
            ..
            context = MvcMockHelpers.MockHttpContext(uri);
            ..
}

@RobBlob
Copy link

RobBlob commented Oct 4, 2017

Very helpful.

Minor comment - in MvcMockHelpers.cs, lines 35-36 and 38-39 are redundant.

@RobBlob
Copy link

RobBlob commented Oct 4, 2017

Further, the ThrowIfNull should not be throwing a NullReferenceException - see the following: https://stackoverflow.com/questions/22453650/why-are-we-not-to-throw-these-exceptions

Prefer an ArgumentNullException, or just let the system throw the NullReferenceException.

@propellingbits
Copy link

Mocking these http objects (request, response etc) is an overkill. We should unit test business components\services. Testing of controllers can be covered via a better approach - Integration Testing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment