Urls for menus in my ASP.NET MVC apps are generated from controller/actions. So, they call
controller.Url.Action(action, controller)
Now, how do I make this work in unit tests?
I use MVCContrib successfully with
var controller = new TestControllerBuilder().CreateController<OrdersController>();
but whatever I try to do with it I get controller.Url.Action(action, controller) failing with NullReferenceException because Url == null.
Update: it's not about how to intercept HttpContext. I did this in several ways, using MVCContrib, Scott Hanselman's example of faking, and also the one from http://stephenwalther.com/blog/archive/2008/07/01/asp-net-mvc-tip-12-faking-the-controller-context.aspx. This doesn't help me because I need to know WHAT values to fake... is it ApplicationPath? How do I set it up? Does it need to match the called controller/action? That is, how do Url.Action works and how do I satisfy it?
Also, I know I can do IUrlActionAbstraction and go with it... but I'm not sure I want to do this. After all, I have MVCContrib/Mock full power and why do I need another abstraction.
A cleaner way to do this is just use Moq(or any other framework you like) to Mock UrlHelper itself
var controller = new OrdersController();
var UrlHelperMock = new Mock<UrlHelper>();
controller.Url = UrlHelperMock.Object;
UrlHelperMock.Setup(x => x.Action("Action", "Controller", new {parem = "test"})).Returns("testUrl");
var url = controller.Url.Action("Action", "Controller", new {parem = "test"});
assert.areEqual("/Controller/Action/?parem=test",url);
clean and simple.
Here's how you could mock UrlHelper using MvcContrib's TestControllerBuilder:
var routes = new RouteCollection();
MvcApplication.RegisterRoutes(routes);
HomeController controller = CreateController<HomeController>();
controller.HttpContext.Response
.Stub(x => x.ApplyAppPathModifier("/Home/About"))
.Return("/Home/About");
controller.Url = new UrlHelper(
new RequestContext(
controller.HttpContext, new RouteData()
),
routes
);
var url = controller.Url.Action("About", "Home");
Assert.IsFalse(string.IsNullOrEmpty(url));
If you're using Moq (and not MvcContrib's TestControllerBuilder), you can mock out the context, similar to #DarianDimitrov's answer:
var controller = new OrdersController();
var context = new Mock<System.Web.HttpContextBase>().Object;
controller.Url = new UrlHelper(
new RequestContext(context, new RouteData()),
new RouteCollection()
);
This doesn't set the controller.HttpContext property, but it does allow Url.Action to execute (and return an empty string -- no mocking required).
Fake it easy works nicely:
var fakeUrlHelper = A.Fake<UrlHelper>();
controller.Url = fakeUrlHelper;
A.CallTo(() => fakeUrlHelper.Action(A<string>.Ignored, A<string>.Ignored))
.Returns("/Action/Controller");
Here is another way to solve the problem with NSubstitute. Hope, it helps someone.
// _accountController is the controller that we try to test
var urlHelper = Substitute.For<UrlHelper>();
urlHelper.Action(Arg.Any<string>(), Arg.Any<object>()).Returns("/test_Controller/test_action");
var context = Substitute.For<HttpContextBase>();
_accountController.Url = urlHelper;
_accountController.ControllerContext = new ControllerContext(context, new RouteData(), _accountController);
Related
Within an MVC project we have an area with a controller SomeController returning a partial view containing EditorFor statements, each with their own template.
Everything works fine if the controller is invoked directly via a route for that area. However, if it's called via another controller outside of the area, i.e. via 'new SomeController().SomeAction()', the templates are not used, even if explicitly specified (the view is returned ok, but shows just default textboxes etc).
What could be the reason for this and how can it be fixed?
When your action is invoked merely using ctrl.Action(), the current RouteData will be used (with the current area/controller/action values in it) and when Razor tries to resolve your EditorTemplates paths it consults the ViewContext that is still containing the (now wrong) values of the originating action.
You better use the ControllerFactory in order to mimic the desired behavior:
var ctrlFactory = ControllerBuilder.Current.GetControllerFactory();
var routeData = new RouteData();
routeData.DataTokens.Add("area", "target_area_name");
routeData.Values.Add("controller", "target_controller_name");
routeData.Values.Add("action", "target_action_name");
var requestContext = new RequestContext(this.HttpContext, routeData);
var ctrl = ctrlFactory.CreateController(requestContext, "target_controller_name") as TargetControllerType;
if (ctrl != null)
{
ctrl.ControllerContext = new ControllerContext(requestContext, ctrl);
var ctrlDesc = new ReflectedControllerDescriptor(typeof(TargetControllerType));
var actionDesc = ctrlDesc.FindAction(ctrl.ControllerContext, "target_action_name");
var result = actionDesc.Execute(ctrl.ControllerContext, new Dictionary<string, object>()) as ActionResult;
this.RouteData.DataTokens["area"] = "target_area_name";
this.RouteData.Values["controller"] = "target_controller_name";
this.RouteData.Values["action"] = "target_action_name";
return result;
}
See MSDN
I found the following answer from Darin Dimitrov - In ASP MVC3, how can execute a controller and action using a uri?
var routeData = new RouteData();
// controller and action are compulsory
routeData.Values["action"] = "index";
routeData.Values["controller"] = "foo";
// some additional route parameter
routeData.Values["foo"] = "bar";
IController fooController = new FooController();
var rc = new RequestContext(new HttpContextWrapper(HttpContext), routeData);
fooController.Execute(rc);
The only problem is that I like to capture the ViewResult that is returned by this Action (to render it as a string), but IController.Execute returns void.
I suspect I can find the Result somewhere in the property of the ControllerContext, but I can't find anything like that. Does anyone have an idea how to do this?
What you want to do, as far as I understand is to actually render the view, get the HTML result and assert against it.
This is practically testing the view which is pretty much not recommended and against most practices.
However, you could come up with some solutions to this. The one presented (simplified and a bit messy) is using RazorEngine to render the view. Since you can't get access to the .cshtml (the view file) from the test project you will need to get to its contents in a messy way.
Install RazorEngine NuGet package to your test project and try something along these lines:
[Fact]
public void Test()
{
var x = new HomeController(); // instantiate controller
var viewResult = (ViewResult)x.Index(); // run the action and obtain its ViewResult
var view = string.IsNullOrWhiteSpace(viewResult.ViewName) ? "Index" : viewResult.ViewName; // get the resulted view name; if it's null or empty it means it is the same name as the action
var controllerName = "Home"; // the controller name was known from the beginning
// actually navigate to the folder containing the views; in this case we're presuming the test project is a sibling to the MVC project, otherwise adjust the path to the view accordingly
var pathToView = Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase).Replace("file:\\", "");
pathToView = Path.GetDirectoryName(pathToView);
pathToView = Path.GetDirectoryName(pathToView);
pathToView = Path.GetDirectoryName(pathToView);
pathToView = Path.Combine(pathToView, "WebApplication5\\Views\\" + controllerName + "\\" + view + ".cshtml");
var html = Razor.Parse(File.ReadAllText(pathToView), viewResult.Model); // this is the HTML result, assert against it (i.e. search for substrings etc.)
}
Having searched StackOverflow, and Google I think what I'm doing is suppose to be right, however results don't seem to be going well
[TestMethod]
public void LoginAction_Should_Return_View_and_User_Authenticated()
{
// Arrange
var mock = new Mock<ControllerContext>();
var mockSession = new Mock<HttpSessionStateBase>();
mock.Setup(p => p.HttpContext.Session).Returns(mockSession.Object);
var testData = FakeUserData.CreateTestUsers();
var repository = new FakeUserRepository(testData);
var controller = new AccountController(repository);
controller.ControllerContext = mock.Object;
// Act
var result = controller.Login("testuser1", "testuser1");
// Assert
Assert.AreEqual("testuser1", controller.HttpContext.Session["Username"]);
Assert.IsTrue((bool)controller.HttpContext.Session["IsAuthenticated"]);
Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));
}
When I run the test the value of controller.HttpContext.Session["Username"] is null, however I set the value to the username using a Session helper. Am I doing something completely wrong, or something else? Any help would be greatly appreciated.
Use Mock.Verify to check if underlying code tried to set Session["Username"].
If your code needs to set session variable and use it - take a look here.
Quickstart is priceless too.
Is there as easy way to convert string URL to RouteValueDictionary collection? Some method like UrlToRouteValueDictionary(string url).
I need such method because I want to 'parse' URL according to my routes settings, modify some route values and using urlHelper.RouteUrl() generate string URL according to modified RouteValueDictionary collection.
Thanks.
Here is a solution that doesn't require mocking:
var request = new HttpRequest(null, "http://localhost:3333/Home/About", "testvalue=1");
var response = new HttpResponse(new StringWriter());
var httpContext = new HttpContext(request, response);
var routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext));
var values = routeData.Values;
// The following should be true for initial version of mvc app.
values["controller"] == "Home"
values["action"] == "Index"
Hope this helps.
You would need to create a mocked HttpContext as routes constrains requires it.
Here is an example that I use to unit test my routes (it was copied from Pro ASP.Net MVC framework):
RouteCollection routeConfig = new RouteCollection();
MvcApplication.RegisterRoutes(routeConfig);
var mockHttpContext = new MockedHttpContext(url);
RouteData routeData = routeConfig.GetRouteData(mockHttpContext.Object);
// routeData.Values is an instance of RouteValueDictionary
//...
I wouldn't rely on RouteTable.Routes.GetRouteData from previous examples because in that case you risk missing some values (for example if your query string parameters don't fully fit any of registered mapping routes). Also, my version doesn't require mocking a bunch of request/response/context objects.
public static RouteValueDictionary UrlToRouteValueDictionary(string url)
{
int queryPos = url.IndexOf('?');
if (queryPos != -1)
{
string queryString = url.Substring(queryPos + 1);
var valuesCollection = HttpUtility.ParseQueryString(queryString);
return new RouteValueDictionary(valuesCollection.AllKeys.ToDictionary(k => k, k => (object)valuesCollection[k]));
}
return new RouteValueDictionary();
}
Here is a solution that doesn't require instantiating a bunch of new classes.
var httpContext = context.Get<System.Web.HttpContextWrapper>("System.Web.HttpContextBase");
var routeData = System.Web.Routing.RouteTable.Routes.GetRouteData(httpContext);
var values = routeData.Values;
var controller = values["controller"];
var action = values["action"];
The owin context contains an environment that includes the HttpContext.
I want to create a route from a utility class that doesn't have access to a ViewContext.
Is this possible? There doesnt seem to be any equivalent of ViewContext.Current
I've tried fishing around in all the constructors for Routing and HttpContext but can't quite get to what I want.
This is what I'm looking for - although this doesn't work because RouteTable.Routes is of type RouteCollection and not RouteData. So close - yet so far :-)
RequestContext requestContext = new RequestContext(HttpContext.Current, RouteTable.Routes);
UrlHelper url = new UrlHelper(requestContext);
var urlString = url.RouteUrl(new {controller="DynamicImage", action="Button", text="Hello World"});
Note: RequestContest is of type System.Web.Routing.RequestContext and not HttpContext
Try this:
var httpContext = new HttpContextWrapper(HttpContext.Current);
var requestContext = new RequestContext(httpContext);
var urlHelper = new UrlHelper(requestContext, new RouteData()));
Hope this helps
UPDATED:
The previous is not correct (I've posted it from my memory). Try this instead (it works in one of my projects):
var httpContext = new HttpContextWrapper(HttpContext.Current);
var requestContext = new RequestContext(httpContext, new RouteData());
var urlHelper = new UrlHelper(requestContext);
new RouteData() is using just only for RequestContext initialization and new UrlHelper(requestContext) actually calls new UrlHelper(requestContext, RouteTable.Routes)