Last active
October 29, 2019 13:43
-
-
Save johnnyreilly/4959924 to your computer and use it in GitHub Desktop.
What you need to unit test MVC controllers using MOQ.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | |
); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
}); | |
} | |
//.... | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")); | |
} | |
} | |
} |
Very helpful.
Minor comment - in MvcMockHelpers.cs, lines 35-36 and 38-39 are redundant.
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.
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
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
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;