I have the following 404 error ActionResult which is thrown when web pages are not found:
public ActionResult InvokeHttp404(HttpContextBase httpContext) {
IController errorController = new ErrorController();
var errorRoute = new RouteData();
errorRoute.Values.Add("controller", "Error");
errorRoute.Values.Add("action", "Http404");
errorRoute.Values.Add("url", httpContext.Request.Url.OriginalString);
errorController.Execute(new RequestContext(httpContext, errorRoute));
return new EmptyResult();
}
And I am attempting to unit test it with the following test:
[TestMethod]
public void Details_Get_404Handler()
{
// Arrange
var controller = GetController(new Repository(), FakeHttpContext());
// Act
var result = controller.Details(3442399) as ViewResult; // invalid Id (not found)
//Assert
Assert.AreEqual("NotFound", result.ViewName);
}
I have been stuck on this test for a long time now, it would throw a null exception on the line of code that requests the Url.OriginalString. After reading around I found this previous post Mocking HttpContextBase with Moq and found the second answer very helpful, as follows: (I added a line that took care of the Url string)
public static HttpContextBase FakeHttpContext()
{
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>();
var user = new Mock<IPrincipal>();
var identity = new Mock<IIdentity>();
request.SetupGet(x => x.Url).Returns(new Uri("http://localhost/a", UriKind.Absolute));
request.Setup(req => req.ApplicationPath).Returns("~/");
request.Setup(req => req.AppRelativeCurrentExecutionFilePath).Returns("~/");
request.Setup(req => req.PathInfo).Returns(string.Empty);
response.Setup(res => res.ApplyAppPathModifier(It.IsAny<string>()))
.Returns((string virtualPath) => virtualPath);
user.Setup(usr => usr.Identity).Returns(identity.Object);
identity.SetupGet(ident => ident.IsAuthenticated).Returns(true);
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);
context.Setup(ctx => ctx.User).Returns(user.Object);
return context.Object;
}
So I have finally beaten the line of code requesting the Url.OriginalString thanks to that post and answer. But I am now stuck on line of code straight after it:
errorController.Execute(new RequestContext(httpContext, errorRoute));. The test now fails with a null reference error on that line. I am slightly confused as to what is null here as I have already faked the httpContext. Could anyone help me with this?
Edit:
Altered ValueProviderFactoriesExtensions:
public static class ValueProviderFactoresExtensions
{
public static ValueProviderFactoryCollection ReplaceNameValueCollectionWith<TOriginal>(this ValueProviderFactoryCollection factories, Func<ControllerContext, NameValueCollection> sourceAccessor)
{
var original = factories.FirstOrDefault(x => typeof(TOriginal) == x.GetType());
if (original != null)
{
var index = factories.IndexOf(original);
factories[index] = new NameValueCollectionProviderFactory(sourceAccessor);
}
return factories;
}
public static ValueProviderFactoryCollection ReplaceHttpFileCollectionWith<TOriginal>(this ValueProviderFactoryCollection factories, Func<ControllerContext, HttpFileCollectionBase> sourceAccessor)
{
var original = factories.FirstOrDefault(x => typeof(TOriginal) == x.GetType());
if (original != null)
{
var index = factories.IndexOf(original);
factories[index] = new HttpFileCollectionProviderFactory(sourceAccessor);
}
return factories;
}
class NameValueCollectionProviderFactory : ValueProviderFactory
{
private readonly Func<ControllerContext, NameValueCollection> sourceAccessor;
public NameValueCollectionProviderFactory(Func<ControllerContext, NameValueCollection> sourceAccessor)
{
this.sourceAccessor = sourceAccessor;
}
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
return new NameValueCollectionValueProvider(sourceAccessor(controllerContext), CultureInfo.CurrentCulture);
}
}
class HttpFileCollectionProviderFactory : ValueProviderFactory
{
private readonly Func<ControllerContext, HttpFileCollectionBase> sourceAccessor;
public HttpFileCollectionProviderFactory(Func<ControllerContext, HttpFileCollectionBase> sourceAccessor)
{
this.sourceAccessor = sourceAccessor;
}
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
return new HttpFileCollectionValueProvider(controllerContext);
}
}
}
Code altered in response to Marnix’s answer:
public static HttpContextBase FakeHttpContext()
{
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>();
var user = new Mock<IPrincipal>();
var identity = new Mock<IIdentity>();
var files = new Mock<HttpFileCollectionBase>();
request.SetupGet(x => x.Url).Returns(new Uri("http://localhost/a", UriKind.Absolute));
request.Setup(req => req.ApplicationPath).Returns("~/");
request.Setup(req => req.AppRelativeCurrentExecutionFilePath).Returns("~/");
request.Setup(req => req.PathInfo).Returns(string.Empty);
request.Setup(req => req.ContentType).Returns("text/html");
request.Setup(req => req.QueryString).Returns(new NameValueCollection());
request.Setup(req => req.Form).Returns(new NameValueCollection());
request.Setup(req => req.Files).Returns(files.Object);
response.Setup(res => res.ApplyAppPathModifier(It.IsAny<string>()))
.Returns((string virtualPath) => virtualPath);
user.Setup(usr => usr.Identity).Returns(identity.Object);
identity.SetupGet(ident => ident.IsAuthenticated).Returns(true);
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);
context.Setup(ctx => ctx.User).Returns(user.Object);
ValueProviderFactories.Factories
.ReplaceNameValueCollectionWith<FormValueProviderFactory>(ctx => ctx.HttpContext.Request.Form)
.ReplaceNameValueCollectionWith<QueryStringValueProviderFactory>(ctx => ctx.HttpContext.Request.QueryString)
.ReplaceHttpFileCollectionWith<HttpFileCollectionValueProviderFactory>(ctx => ctx.HttpContext.Request.Files);
return context.Object;
}
Still can’t get it to work, still throws a null reference on the same line of code. Don’t know if stack trace is struggling to find view? stack trace:
System.Web.Mvc.ViewResult.FindView(ControllerContext context)
Mocking up the context for a controller is a bit tricky. In MVC3 there’s an additional step to prevent the value providers from touching HttpContext.Current.
This answer explains what you need to do.
Update: You probably don use the ValueProviderFactories directly, however the ActionInvoker on the controller does. Somewhere deep down the default implementation for the value providers accesses HttpContext.Current which is not set outside of a web request, causing a NullReferenceException during unit tests. You can prevent this by replacing the default value providers (as described in the linked answer).
Copy the
ValueProviderFactoresExtensionsclass from the link above into your test project add the following code to yourFakeHttpContextmethod: