Skip to main content

Another Look at Unit Testing Umbraco Surface Controllers (Part 2)

With the release of Umbraco 7.3, this has got a whole lot simpler - see my more recent post discussing this topic

In the previous post I looked at testing Umbraco surface controllers, using a technique that basically avoided the issue by extracting the non-Umbraco related logic for test into a separate class, and testing that.

Another option is to test the controller as is, using the base test classes provided by the Umbraco core team.

My starting point for working with these was a blog post by Jorge Lusar where he details much of the process for working with these. He used Umbraco 6.1.5, and I found things had changed a little in 7.1.0 (latest at time of writing), as well as needing some additional work to handle the controller methods I was looking to test.

As Jorge notes, the first things is to get the test classes. They aren't currently available as a separate download as far as I'm aware, so instead I've pulled down and built the latest source code, and then copied Umbraco.Tests.dll to my solution and referenced it from my test project.

I tend to use MSTest for my unit testing, but these base classes are based on using NUnit, so the first thing was to switch to that.

To recap, this was the controller method I was looking to test:

[HttpPost]
public ActionResult CreateComment(CommentViewModel model)
{
    if (!ModelState.IsValid)
    {
        return CurrentUmbracoPage();
    }

    TempData.Add("CustomMessage", "Thanks for your comment.");

    return RedirectToCurrentUmbracoPage();
}

And I have these two tests:

[Test]        
public void CreateComment_WithValidComment_RedirectsWithMessage()
{
    // Arrange
    var controller = GetController();
    var model = new CommentViewModel
    {
        Name = "Fred",
        Email = "fred@freddie.com",
        Comment = "Can I test this?",
    };

    // Act
    var result = controller.CreateComment(model);

    // Assert
    var redirectToUmbracoPageResult = result as RedirectToUmbracoPageResult;
    Assert.IsNotNull(redirectToUmbracoPageResult);
    Assert.AreEqual(1000, redirectToUmbracoPageResult.PublishedContent.Id);
    Assert.IsNotNull(controller.TempData["CustomMessage"]);
}

[Test]
public void CreateComment_WithInValidComment_RedisplaysForm()
{
    // Arrange
    var controller = GetController();
    var model = new CommentViewModel
    {
        Name = "Fred",
        Email = string.Empty,
        Comment = "Can I test this?",
    };
    controller.ModelState.AddModelError("Email", "Email is required.");

    // Act
    var result = controller.CreateComment(model);

    // Assert
    var umbracoPageResult = result as UmbracoPageResult;
    Assert.IsNotNull(umbracoPageResult);
    Assert.IsNull(controller.TempData["CustomMessage"]);
}

In order to get this working I needed to make one change to my controller, adding a second constructor that allows the passing in of an instance of an UmbracoContext, as follows:

public BlogPostSurfaceController(UmbracoContext ctx) : base(ctx)
{
}

One change from Jorge's method is that now rather than indicating to the base test classes that a database is not required by overriding a method - if it is, the base classes support set up of a SQL CE database instance - an attribute is added either to the test class or method. In my case I didn't need a database so could use this version of the attribute, and as you can see the test class itself inherits from BaseRoutingTest which is provided by the Umbraco base test classes.

[TestFixture]
[DatabaseTestBehavior(DatabaseBehavior.NoDatabasePerFixture)]
public class BlogPostSurfaceControllerTestsWithBaseClasses : BaseRoutingTest
{
    ...
}

In order to set up the necessary contexts for the test I've adapted Jorge's method GetController() which you can see called from the test methods above. The code for this is as follows, with further explanation below, as it got just a bit complex at this point!

private BlogPostSurfaceController GetController()
{
    // Create contexts via test base class methods
    var routingContext = GetRoutingContext("/test");
    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(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, MockIPublishedContent(), 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 = new BlogPostSurfaceController(umbracoContext);
    controller.ControllerContext = new ControllerContext(contextBase, routeData, controller);
    controller.Url = new UrlHelper(new RequestContext(contextBase, new RouteData()), new RouteCollection());
    return controller;
}

private IPublishedContent MockIPublishedContent()
{
    var mock = new Mock<IPublishedContent>();
    mock.Setup(x => x.Id).Returns(1000);
    return mock.Object;
}

Having first taken Jorge's method, I ran into problems with the call to RedirectToCurrentUmbracoPage() in the controller method. This in turn makes a call to the CurrentPage property and that would fail without an appropriate value in the umbraco-route-def key of the controller's route data.

I've added that, but unfortunately the value for this key is an instance of a RouteDefinition class, provided by Umbraco but marked as internal. In order to create an instance of that, you have to resort to using reflection, which I've done above by referencing the assembly, activating an instance of the type, and setting the necessary properties.

Similarly, a property of RouteDefinition - PublishedContentRequest - whilst a public class, has an internal constructor. We can again use reflection though to create an instance of this, and set it's properties - in this case an instance of IPublishedContent that can be mocked using Moq.

The final step to getting this to work was to ensure the appropriate configuration files are in place - web.config and umbracoSettings.config. These I copied from the Umbraco.Tests project from source, added them to my project and set their Copy to Output Directory property to Copy Always, so they would be found in the location that the base test classes expect

Having done all this, the tests pass.

Conclusions

There is an important caveat with using this method which means we might question on using it in production. Classes, constructors and methods are marked as internal for a reason by the Umbraco core team, namely that by doing so they can safely change them without worrying about changing a public interface that might affect people's applications. You can get round this using reflection as I have, but it's at risk of breaking following any upgrade to Umbraco, even minor ones.

That said, this code is nicely isolated and only needs to be set up once for testing surface controllers, and so could likely be fairly easily updated should these internal classes change.

If any one wants to review the code for this further you can find it in this Github repository.

Comments

  1. Hi,
    Just a question: from what reference comes this method (I tried Umbraco.Tests.TestHelpers)?

    var routingContext = GetRoutingContext("/test");

    ReplyDelete
  2. Sorry, this post is quite old now and don't quite recall. See the link at the top though as would suggest there are easier ways to be unit testing with Umbraco these days. Worth checking out Lars-Erik's blog too - he has a lot of good stuff on the subject.
    http://blog.aabech.no/

    ReplyDelete

Post a Comment