Thursday, 27 March 2014

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

When coming to work with Umbraco as an MVC developer one feature that is immediately familiar and comfortable to work with is surface controllers. Unfortunately, unlike standard MVC controllers, they aren't straightforward to test. In this blog post I'm going to look to start from scratch, uncover the issues and see what options we have to get around this.

Failing test attempt

Starting with a simple example taken from the Umbraco documentation, this surface controller action very simply handles a form post.

The view model:

public class CommentViewModel
{
    [Required]
    public string Name { get; set; }

    [Required]
    public string Email { get; set; }

    [Required]
    [Display(Name = "Enter a comment")]
    public string Comment { get; set; }
}

The view and the form in a partial:

@if (TempData["CustomMessage"] != null)
{
    

@TempData["CustomMessage"].ToString()

} @Html.Partial("_CommentForm", new SurfaceControllerUnitTests.Models.CommentViewModel()) ... @model SurfaceControllerUnitTests.Models.CommentViewModel @using (Html.BeginUmbracoForm("CreateComment", "BlogPostSurface")) { @Html.EditorFor(x => Model) <input type="submit" /> }

And the controller:

public class BlogPostSurfaceController : Umbraco.Web.Mvc.SurfaceController
{
    [HttpPost]
    public ActionResult CreateComment(CommentViewModel model)
    {    
        if (!ModelState.IsValid)
        {
            return CurrentUmbracoPage();
        }

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

        return RedirectToCurrentUmbracoPage();
    }
}

Based on that we can write this test:

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

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

    // Assert
    Assert.IsNotNull(result);
}

Which... fails, with the exception:

System.ArgumentNullException: Value cannot be null.
Parameter name: umbracoContext
Result StackTrace: 
at Umbraco.Web.Mvc.PluginController..ctor(UmbracoContext umbracoContext)
   at Umbraco.Web.Mvc.SurfaceController..ctor()
   at SurfaceControllerUnitTests.Controllers.BlogPostSurfaceController..ctor()

Avoiding the issue

One way to approach this is really to avoid the issue which I'm blogged about briefly before. To move the logic the controller performs out into a separate class that we reference from the controller, that doesn't have an Umbraco dependency. We can then test that. Leaving behind a controller that's so thin there's really little value in testing it.

Now clearly the controller I already have is pretty thin, but sometimes the logic here that goes into the handler class can get quite involved - a registration form that needs validation, date checks, CAPTCHA checks etc. So I've found this a reasonable approach.

The new handler class looks like this:

public class BlogPostSurfaceControllerCommandHandler
{
    public ModelStateDictionary ModelState { get; set; }

    public TempDataDictionary TempData { get; set; }

    public bool HandleCreateComment(CommentViewModel model)
    {
        if (!ModelState.IsValid)
        {
            return false;
        }

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

With the controller becoming like this (or you could dependency inject the handler instance):

public class BlogPostSurfaceController : Umbraco.Web.Mvc.SurfaceController
{
    BlogPostSurfaceControllerCommandHandler _commandHandler;

    public BlogPostSurfaceController()
    {
        _commandHandler = new BlogPostSurfaceControllerCommandHandler();
        _commandHandler.ModelState = ModelState;
        _commandHandler.TempData = TempData;
    }

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

        return RedirectToCurrentUmbracoPage();
    }
}

And the test for the handler looks like this - which, as expected, now comes out green.

[TestClass]
public class BlogPostSurfaceControllerCommandHandlerTests
{
    [TestMethod]
    public void CreateComment_WithValidComment_ReturnsTrueWithMessage()
    {
        // Arrange
        var handler = new BlogPostSurfaceControllerCommandHandler();
        handler.ModelState = new ModelStateDictionary();
        handler.TempData = new TempDataDictionary();
        var model = new CommentViewModel
        {
            Name = "Fred",
            Email = "fred@freddie.com",
            Comment = "Can I test this?",
        };

        // Act
        var result = handler.HandleCreateComment(model);

        // Assert
        Assert.IsTrue(result);
        Assert.IsNotNull(handler.TempData["CustomMessage"]);
    }
}

Using Umbraco's test classes

So far have only read about this option - so will come back to it in part 2 of this series of posts..

No comments:

Post a Comment