Unit testing our Umbraco solutions has always been somewhat of a challenge, mostly which stemmed from difficulties of isolating individual 'units' of functionality from their dependencies within the bounds of Umbraco's best practices.
Since Umbraco 7.3.4, some of this friction has been removed thanks to the addition of a few extra methods within the core of Umbraco, and also some very helpful community packages. In this article I'll walk us through stitching a few of these together and writing our own testable Umbraco controllers.
Solution Setup
Typically my solutions will contain a few projects, but at a basic level I'll have a "MyProject.Web" which contains my Umbraco installation, models, controllers and views; and also "MyProject.Tests" with my corresponding unit tests.
Umbraco Context Mock
Umbraco Context Mock is a simple NuGet package which I originally published in 2015. The aim of Umbraco Context Mock was simple: create a small set of helpers aimed to both save developers the repetitive task of creating the same fake Umbraco context instance in their tests, and also to gently guide them into a pattern of writing testable code without strong arming restrictive design patterns.
To install Umbraco Context Mock, we will install both the Web and Testing packages respectively. For our Tests project, we will execute the following via the Package Manager Console:
PM> Install-Package GDev.Umbraco.Testing
And in both our Umbraco project and our test project, we will install the references to the Umbraco Context Mock web library:
PM> Install-Package GDev.Umbraco.Testing.Web
Umbraco.Tests
Although Umbraco has made great strides in making the codebase testable for us third party developers, there are still a number of roadblocks in the way (these are mostly caused by un-mockable classes from inside Umbraco's core). To work around these roadblocks we would have to checkout the core source from GitHub, build the project and copy out the Umbraco.Tests.dll file.
Luckily, thanks to Christian Thillemann creating an automatic nuget build - this has now been made a lot quicker, and friendlier. We can go ahead and install the Umbraco tests package in to our own "Tests" project:
PM> Install-Package Our.Umbraco.Community.Tests
For the purposes of the following examples, I will also install the NUnit testing framework in my "Tests" project, though you are free to switch this out with whichever unit testing framework you prefer.
PM> Install-Package NUnit
After installing all four of these packages, we should be ready to write some code!
Writing and fulfilling a test
As almost anyone who has discussed testing with me will confirm I am a big believer in test driven development (TDD), and maybe even express my opinions rather strongly on the subject. The biggest benefit that draws me to this approach is that we can easily transform our feature user stories into a unit test criteria.
For the purposes of this example, we are going to determine whether or not a comments section of an article should be displayed on the page based on the following combinations of factors.
- If comments are enabled on a page, a user can see the comment box.
- If comments are disabled on a page, a user cannot see the comment box.
To illustrate this in the most verbose way possible, I decided to create a model for my article page.
using System;
namespace Skrift.Web.Models
{
public class Article
{
public string Title { get; set; }
public string Content { get; set; }
public DateTime Posted { get; set; }
public bool ShowComments { get; set; }
}
}
I have also created a skeleton controller. Notice that the controller is inheriting BaseRenderMvcController instead of RenderMvcController and has an additional constructor. This is part of the Umbraco Context Mock pattern that I am utilising in order to allow me to create unit tests with the least possible friction.
Note that BaseRenderMvcController inherits RenderMvcController itself, so we still have access to all RenderMvcController properties and methods, and will automatically pick up future improvements to the core.
using GDev.Umbraco.Web.Controllers;
using System.Web.Mvc;
using Umbraco.Web;
namespace Skrift.Web.Controllers
{
public class ArticleController : BaseRenderMvcController
{
public ArticleController(UmbracoContext context, UmbracoHelper helper) : base(context, helper) { }
public ActionResult Index()
{
return View();
}
}
}
Now that we have our basic structure in place, we can start to transform our comment box criteria into a unit test.
using GDev.Umbraco.Test;
using Moq;
using NUnit.Framework;
using Skrift.Web.Controllers;
using Skrift.Web.Models;
using System.Web.Mvc;
using Umbraco.Core.Models;
using Umbraco.Web;
namespace Skrift.Tests.Controllers
{
[TestFixture]
public class ArticleControllerTests
{
private ContextMocker _mocker;
private Mock<IPublishedContent> _currentPage;
[SetUp]
public void Setup()
{
// Our Umbraco Context Mock "Mocker" will give us a fake context
// Instead of having to write our Umbraco.EnsureContext method
this._mocker = new ContextMocker();
this._currentPage = new Mock<IPublishedContent>();
}
[Test]
[TestCase(true)]
[TestCase(false)]
public void UserCanSeeCommentBox(bool enabled)
{
// Ensure that getPropertyValue for commentsEnabled returns the expected boolean value
this._currentPage.Setup(x => x.GetProperty("commentsEnabled").Value).Returns(enabled);
// Create a fake UmbracoHelper that we can determine the values of
var helper = new UmbracoHelper(this._mocker.UmbracoContextMock, this._currentPage.Object);
// Create an instance of the controller, and pass in our fake context and helper
var controller = new ArticleController(this._mocker.UmbracoContextMock, helper);
var result = (ViewResult)controller.Index();
Article model = (Article)result.Model;
Assert.AreEqual(enabled, model.ShowComments);
}
}
}
This may look daunting at first, but lets pick through the methods to figure out what is actually going on.
Our Setup method (marked with the [SetUp] attribute) will run before each and every test in our test class.
[SetUp]
public void Setup()
{
// Our Umbraco Context Mock "Mocker" will give us a fake context
// Instead of having to write our Umbraco.EnsureContext method
this._mocker = new ContextMocker();
this._currentPage = new Mock<IPublishedContent>();
}
In here we are simply creating a new instance of our Umbraco Context Mock "mocker". This will give us access to a fake UmbracoContext object, and a fake ApplicationContext object which will help us to execute our controller code in isolation and without any Umbraco core interference.
Secondly we are creating a Mock of IPublishedContent. This will allow us to manipulate which values are returned from .GetProperty whilst still testing in isolation from the Umbraco database and content cache.
Next we have our actual test. As we are testing the same functionality with different enabled/disabled states, I am leveraging NUnit's [TestCase()] attribute to allow us to use the same test for both states.
[Test]
[TestCase(true)]
[TestCase(false)]
public void UserCanSeeCommentBox(bool enabled)
Firstly, we are setting up our IPublishedContent object to return true/false (depending on which test case is being run) when we make a call to .GetProperty("commentsEnabled").Value.
// Ensure that getPropertyValue for commentsEnabled returns the expected boolean value
this._currentPage.Setup(x => x.GetProperty("commentsEnabled").Value).Returns(enabled);
Next we have to create our own instance of UmbracoHelper, we can create this in isolation by using our fake context from our ContextMocker object, and we will also set the current IPublishedContent node to be our _currentPage object which we have just setup.
// Create a fake UmbracoHelper that we can determine the values of
var helper = new UmbracoHelper(this._mocker.UmbracoContextMock, this._currentPage.Object);
Now that we have our constructing objects that are required to create an Umbraco request set up we can create an instance of our controller. Thanks to utilising our BaseRenderMvcController in our ArticleController these fake objects can be passed through our constructor and assigned automatically.
// Create an instance of the controller, and pass in our fake context and helper
var controller = new ArticleController(this._mocker.UmbracoContextMock, helper);
And lastly, we will call our Index() action to receive the model which we return has the .ShowComments property value equal to the "enabled" state which we expected.
var result = (ViewResult)controller.Index();
Article model = (Article)result.Model;
Assert.AreEqual(enabled, model.ShowComments);
Now that we have our fully constructed test cases, we can run them to ensure we aren't recieving any false positives and get on to writing our controller logic.
using GDev.Umbraco.Web.Controllers;
using Skrift.Web.Models;
using System.Web.Mvc;
using Umbraco.Core.Models;
using Umbraco.Web;
namespace Skrift.Web.Controllers
{
public class ArticleController : BaseRenderMvcController
{
public ArticleController(UmbracoContext context, UmbracoHelper helper) : base(context, helper) { }
public ActionResult Index()
{
IPublishedContent content = Umbraco.AssignedContentItem;
Article viewModel = new Article
{
ShowComments = (bool)content.GetProperty("commentsEnabled").Value
};
return View(viewModel);
}
}
}
As you can see I have written some rather verbose code to illustrate how we can write just a few lines to satisfy our test, and after running our test again both cases should now be passing!
Conclusion
After following along with this example, you should hopefully have a foothold in the world of Umbraco unit testing which you can build upon and start integrating this development process into your project builds. Not only will you start find your regression bugs quicker and have more confidence in your build deployments, but you will inherently begin to build more maintainable code as a by-product.
Undoubtedly there are some areas of Umbraco unit testing which are still quite grey and can get extremely complex, but the community and the core team are continuously making progress in order to make these tasks easier and more available.
If you are interested in contributing to Umbraco Context Mock, you can find it on GitHub: https://github.com/garydevenay/Umbraco-Context-Mock