Test a Controller POST Request

How to Test a SurfaceController on a POST Request

Umbraco unit testing TDD Tuesday, May 20, 2014

When testing an ActionResult that is an UmbracoPageResult or RedirectToUmbracoPageResult, simply instantiating a controller with the umbraco context in the tests class is no longer enough. I got the following code snippet from Andy Butland's blog in a post he wrote about testing surface controllers.

The thing, if you happen to want to use methods like RedirectToCurrentUmbracoPage or CurrentUmbracoPage instead of View and you use the approach as outlined in the previous post about testing controllers, you'll end up getting an exception in your unit tests:

System.InvalidOperationException : Cannot find the Umbraco route definition in the route values, the request must be made in the context of an Umbraco request

Obviously, the controller is missing something that these methods use. The top of the stack gives us a clue. There isn't a current page defined.

Result StackTrace: 
at Umbraco.Web.Mvc.SurfaceController.get_CurrentPage()
at Umbraco.Web.Mvc.SurfaceController.RedirectToCurrentUmbracoPage()

No worries though. The following code snippet takes care of creating the route data with information about the current page. I wrapped the original code inside my MocksFactory.CreateController method but I left Andy's original comments on to explain what is happening here.

/// <summary>
/// Gets the stubbed controller such that we can use and test RedirectToUmbracoTemplate method (and other similar ones).
/// </summary>
/// <param name="routingContext">The routing context.</param>
/// <param name="createController">The function used to create the controller (so we can use this method to create any controller regardless of constructor signature).</param>
/// <param name="publishedContent">The current page published content.</param>
/// <remarks>
///     Inspired in <see cref="http://web-matters.blogspot.co.uk/2014/04/unit-testing-umbraco-with-base-test-classes.html"/>.
///     I retained original comments.
/// </remarks>
/// <returns>The stubbed controller and associated object.</returns>
public static TController CreateController<TController>(
    Func<TController> createController,
    RoutingContext routingContext,
    IPublishedContent publishedContent) where TController : Controller
{
    // Create contexts via test base class methods
    var umbracoContext = routingContext.UmbracoContext;
    var contextBase = umbracoContext.HttpContext;

    // We need to add a value to the controller's RouteData, otherwise calls to CurrentPage
    // (or RedirectToCurrentUmbracoPage) will fail

    // Unfortunately some types and constructors necessary to do this are marked as internal

    // Create instance of RouteDefinition class using reflection
    // - note: have to use LoadFrom not LoadFile here to type can be cast (http://stackoverflow.com/questions/3032549/c-on-casting-to-the-same-class-that-came-from-another-assembly
    var assembly = Assembly.LoadFrom(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "umbraco.dll"));
    var reflectedRouteDefinitionType = assembly.GetType("Umbraco.Web.Mvc.RouteDefinition");
    var routeDefinition = Activator.CreateInstance(reflectedRouteDefinitionType);

    // Similarly create instance of PublishedContentRequest
    // - note: have to do this a little differently as in this case the class is public but the constructor is internal
    var reflectedPublishedContentRequestType = assembly.GetType("Umbraco.Web.Routing.PublishedContentRequest");
    var flags = BindingFlags.NonPublic | BindingFlags.Instance;
    var culture = CultureInfo.InvariantCulture;
    var publishedContentRequest = Activator.CreateInstance(reflectedPublishedContentRequestType, flags, null, new object[] { new Uri("/test", UriKind.Relative), routingContext }, culture);

    // Set properties on reflected types (not all of them, just the ones that are needed for the test to run)
    var publishedContentRequestPublishedContentProperty = reflectedPublishedContentRequestType.GetProperty("PublishedContent");
    publishedContentRequestPublishedContentProperty.SetValue(publishedContentRequest, publishedContent, null);
    var publishedContentRequestProperty = reflectedRouteDefinitionType.GetProperty("PublishedContentRequest");
    publishedContentRequestProperty.SetValue(routeDefinition, publishedContentRequest, null);

    // Then add it to the route data tht will be passed to the controller context
    // - without it SurfaceController.CurrentPage will throw an exception of: "Cannot find the Umbraco route definition in the route values, the request must be made in the context of an Umbraco request"
    var routeData = new RouteData();
    routeData.DataTokens.Add("umbraco-route-def", routeDefinition);

    // Create the controller with the appropriate contexts
    var controller = createController();
    controller.ControllerContext = new ControllerContext(contextBase, routeData, controller);
    controller.Url = new UrlHelper(new RequestContext(contextBase, new RouteData()), new RouteCollection());

    return controller;
}

It is likely that future releases of Umbraco will have a public API to do something like this and it is not guaranteed that this method will continue to work. Since reflection is used to get internal APIs, the Umbraco team is not guaranteed to deprecate the APIs. They may change at anytime in the future but my guess is that that will not happen fast.