The basics of unit testing Umbraco
Unit testing Umbraco has been a challenging endeavor through the years. However, over the past two-three years, a bright light has started shining from the inner core of Umbraco. It slowly and steadily slips through to more and more of the outermost layers when dependency after dependency is being inverted. Yes, we're talking dependency inversion. Like 99% of all other software on the planet, Umbraco has had it's share of close couplings and dependent code. Luckily, the guys at HQ are good at what they're doing, and they're fixing it class by class. (You can help too).
So what does that mean for us? Well, for the most part, we can now test all of our code. Even the code that depends heavily on Umbraco. There are things you should stop doing and things you should start doing, but more on that later. Let's have a look at some code so you don't get bored first. Let's start with a surface controller. It should map some content to a simple view model. Let's create a view that shows a date and an author. It will either get it from the built-in node properties, or from custom properties created to override the default ones.
We'll be really conscious testers and start with a test. We'll make sure we can get the built-in properties:
[TestFixture]
class Stubbing_Author_And_Date
{
[Test]
public void Built_In_Properties_Are_Used_By_Default()
{
var bylineController = new BylineController();
var result = bylineController.Byline();
var model = (BylineModel) result.Model;
Assert.IsNotNullOrEmpty(model.Author);
Assert.IsNotNull(model.Date);
}
}
We'll throw together a really dumb controller for now just to make it build:
public class BylineController : SurfaceController
{
public PartialViewResult Byline()
{
return PartialView("Byline", new BylineModel("An author", DateTime.Today));
// BylineModel omitted for simplicity
}
}
What happens in the test is that we make sure calling Byline
on our controller will return a model with values. We didn't access Umbraco, so this test should just pass with flying colors. However, if we run this test, it won't even reach the Assert
call. It will blow up at the controller constructor. This is the exception we get from running the test:
System.ArgumentNullException : Value cannot be null.
Parameter name: umbracoContext
at Umbraco.Web.Mvc.PluginController..ctor(UmbracoContext umbracoContext) in C:\Users\Lars-Erik\Source\Repos\Open Source\Umbraco-CMS\Umbraco-CMS\src\Umbraco.Web\Mvc\PluginController.cs:line 31
at Umbraco.Web.Mvc.SurfaceController..ctor() in C:\Users\Lars-Erik\Source\Repos\Open Source\Umbraco-CMS\Umbraco-CMS\src\Umbraco.Web\Mvc\SurfaceController.cs:line 37
at Umb.Testing.Web.Controllers.BylineController..ctor()
at Umb.Testing.Tests.SampleUmbracoTests.Stubbing_Author_And_Date.Built_In_Properties_Are_Used_By_Default() in C:\Users\Lars-Erik\Source\Repos\Open Source\Umbraco Unit Testing\Umb.Testing.Tests\SampleUmbracoTests\Stubbing_Author_And_Date.cs:line 18
Turns out that the SurfaceController
class we derive from access UmbracoContext.Current
. It passes it to its base PluginController
's constructor. We need to make it so that UmbracoContext.Current
is available before we instantiate the controller. Luckily Umbraco has been opened up a bit, so we are able to set up the UmbracoContext
. We could create a [SetUp]
method for NUnit to run before our tests like so:
[SetUp]
public void SetUp()
{
var applicationContext = new ApplicationContext(
CacheHelper.CreateDisabledCacheHelper(),
new ProfilingLogger(Mock.Of<ILogger>(), Mock.Of<IProfiler>())
);
UmbracoContext.EnsureContext(
Mock.Of<HttpContextBase>(),
applicationContext,
new WebSecurity(Mock.Of<HttpContextBase>(), applicationContext),
Mock.Of<IUmbracoSettingsSection>(),
Enumerable.Empty<IUrlProvider>(),
true
);
}
If you're using ReSharper or a similar tool, it'll help you create all the using statements you'll need for this. The Mock
class comes from a Nuget package you need called "Moq". (Use version 4.1.1309.919)
If we run the test now, it passes (with flying colors). Although that's a huge load of weird setup code. There's a whole lot that isn't related to our test and logic. We could probably throw it into a helper library or a base class and re-use it, but there is an issue with that. Have you ever heard the term re-inventing the wheel being used about software? This wheel has definitely been invented before. The guys who made Umbraco invented it. It's an assembly called Umbraco.Tests. As of writing, it's available by building it from the Umbraco source. There is an issue on the tracker to publish it as a Nuget package, so feel free to vote. If you build it though, you can copy the Umbraco.Tests.dll and .pdb files to a lib folder in your solution and check that into your SCM. Referencing it lets us rewrite our class and setup method like so:
[TestFixture]
public class Stubbing_Author_And_Date : BaseRoutingTest
{
[SetUp]
public void SetUp()
{
GetUmbracoContext("http://localhost", -1, setSingleton: true);
}
// ...
And the test still pass. We swapped out the big mess with one call to GetUmbracoContext
. It's a method we get from inheriting BaseRoutingTest
. Notice the last argument setSingleton
. It's an optional one, and it tells Umbraco.Tests to set the UmbracoContext.Current
singleton. In order to test our code in isolation, which is a rather noble goal when unit testing, we should try to rid ourselves of using big singletons. There is another way to go about it. SurfaceController
and its base classes have overloaded constructors that lets you inject the UmbracoContext
instead. It may also take an UmbracoHelper
which will be used as its this.Umbraco
instance. This gives us the power to inject any instance of these classes and control the state for the controller. Let's try to fetch the author from the CurrentPage
of the SurfaceController before we do anything else:
public class BylineController : SurfaceController
{
public PartialViewResult Byline()
{
return PartialView("Byline", new BylineModel(CurrentPage.WriterName, DateTime.Today));
}
}
Running the test now makes it blow up again:
System.InvalidOperationException : Cannot find the Umbraco route definition in the route values, the request must be made in the context of an Umbraco request
There's a nasty exception going on about routing and contexts and whatnot. If we dive into the CurrentPage
property we find that it tries to access an ancestor view context to find its CurrentPage
. Let's not try to stub that stuff up. There is another artifact that usually is the same IPublishedContent
as CurrentPage
. It's the UmbracoHelper.AssignedContentItem
property. As you can remember, we can inject an UmbracoHelper
into the controller. An UmbracoHelper
can have the UmbracoContext
and an IPublishedContent
injected, and the latter will be used as AssignedContentItem
. If you're not familiar with stubbing, you're probably questioning how the heck we're supposed to create an IPublishedContent. We could write a fake implementation, but it would require a lot of code. A better option is to utilize the Moq framework we got from Nuget. We can say Mock.Of<IPublishedContent>()
and voilĂ : out comes a runtime generated instance of an implementation of the IPublishedContent
interface. Here's the modified test with everything injected:
private IPublishedContent content;
private UmbracoContext umbracoContext;
private UmbracoHelper umbracoHelper;
[SetUp]
public void SetUp()
{
content = Mock.Of<IPublishedContent>();
umbracoContext = GetUmbracoContext("http://localhost", -1);
umbracoHelper = new UmbracoHelper(umbracoContext, content);
}
[Test]
public void Built_In_Properties_Are_Used_By_Default()
{
var bylineController = new BylineController(umbracoContext, umbracoHelper);
// ...
We'll also update the controller so it gets the WriteName
from our AssignedContentItem
:
public PartialViewResult Byline()
{
return PartialView("Byline", new BylineModel(Umbraco.AssignedContentItem.WriterName, DateTime.Today));
}
We're close, but no cigar yet. This test will fail. We can't create the UmbracoHelper
using the UmbracoContext
we get from BaseRoutingTest
. The UmbracoHelper
is complaining there's no RoutingContext
on the UmbracoContext
. This is why we inherited BaseRoutingTest
. Umbraco.Tests has quite a few useful base classes for testing, and the BaseRoutingTest
class is the one you need to test your MVC stuff. Since we're using it, we can replace our call to GetUmbracoContext
with a call to GetRoutingContext
. However it needs the Umbraco settings around. That's another wheel already invented, and we can get them from a nifty class called SettingsForTests
:
[SetUp]
public void SetUp()
{
content = Mock.Of<IPublishedContent>();
var settings = SettingsForTests.GenerateMockSettings();
var routingContext = GetRoutingContext("http://localhost", -1, umbracoSettings:settings);
umbracoContext = routingContext.UmbracoContext;
umbracoHelper = new UmbracoHelper(umbracoContext, content);
}
The RoutingContext
exposes the same UmbracoContext
that we had, and it also makes sure the UmbracoContext
has a RoutingContext
. Running the test now actually let's it get all the way to our assertions. The exception is finally something we expect:
Expected: not null or empty string
But was: null
We'll obviously have to set them to something. It would be tempting to just do it:
const string expectedAuthor = "An author";
content.WriterName = expectedAuthor;
However, we soon get a red curly line telling us that WriteName has no setter. Incidentally those mocks we get from Moq are pretty cool things. We can instruct them to return data. This is actually called stubbing, not mocking, but that's for another discussion. To be able to do it, we'll have to get a "mock" from the instance:
var contentMock = Mock.Get(content);
A good practice when unit-testing is to put your expected values in constants so you know they're not inadvertently changed during the test. We can instruct the mock object to return a constant value by using a Setup()
and Return()
chain:
const string expectedAuthor = "An author";
var contentMock = Mock.Get(content);
contentMock.Setup(c => c.WriterName).Returns(expectedAuthor);
Now, whenever we access content.WriterName
, it'll return our expected value. Let's do the same thing to Date. We'll also update our assertions so they compare the values of our model to the expected ones:
[Test]
public void Built_In_Properties_Are_Used_By_Default()
{
const string expectedAuthor = "An author";
const string expectedDate = "2015-12-31";
var contentMock = Mock.Get(content);
contentMock.Setup(c => c.WriterName).Returns(expectedAuthor);
contentMock.Setup(c => c.CreateDate).Returns(DateTime.Parse(expectedDate));
var bylineController = new BylineController(umbracoContext, umbracoHelper);
var result = bylineController.Byline();
var model = (BylineModel) result.Model;
Assert.AreEqual(expectedAuthor, model.Author);
Assert.AreEqual(expectedDate, model.Date.Value.ToString("yyyy-MM-dd"));
}
We can run the test, and it fails on the last line. It's pretty obvious. We "forgot" to change DateTime.Today
in the controller Let's fix it:
public PartialViewResult Byline()
{
var content = Umbraco.AssignedContentItem;
return PartialView("Byline", new BylineModel(content.WriterName, content.CreateDate));
}
Finally, the test passes with flying colors (again). But we've only satisfied one of the requirements. We need to get the overriding properties from the content. We'd also like to get that date as a date, so we'll use the GetPropertyValue<T>()
extension. Since it's an extension it's a static method, and that's something we're (sadly) not able to stub. We'll have to stub something else. If you use ReSharper, look at the source, or use some other means of disassembly, you can check out what GetPropertyValue<T>()
actually does. One might think it iterates the Properties
collection of IPublishedContent
and stub that, but it wouldn't help. It calls the GetProperty(string, bool)
method and accesses the returned IPublishedProperty
's value.
Let's write another test that asserts that we can override the author and date. While doing that, we also extract the stubbing of the content to it's own method. Further, the new test also stubs up the IPublishedProperties
we need, using another small helper:
[Test]
public void Content_Properties_Override_Bultin_Properties()
{
const string expectedAuthor = "An author";
const string expectedDate = "2015-12-31";
StubContent("Another author", "2016-01-01");
StubProperty("author", expectedAuthor);
StubProperty("date", expectedDate);
// creating BylineController moved to SetUp.
var result = bylineController.Byline();
var model = (BylineModel) result.Model;
Assert.AreEqual(expectedAuthor, model.Author);
Assert.AreEqual(expectedDate, model.Date.Value.ToString("yyyy-MM-dd"));
}
private void StubContent(string expectedAuthor, string expectedDate)
{
var contentMock = Mock.Get(content);
contentMock.Setup(c => c.WriterName).Returns(expectedAuthor);
contentMock.Setup(c => c.CreateDate).Returns(DateTime.Parse(expectedDate));
}
private void StubProperty<T>(string alias, T value)
{
var prop = new Mock<IPublishedProperty>();
prop.Setup(p => p.Value).Returns(value);
Mock.Get(content).Setup(c => c.GetProperty(alias, false)).Returns(prop.Object);
}
(Notice the second way to go about stubbing with Moq)
We'll need to add the logic to the controller as well to get it to pass:
public PartialViewResult Byline()
{
var content = Umbraco.AssignedContentItem;
return PartialView("Byline", new BylineModel(
content.GetPropertyValue<string>("author").IfNullOrWhiteSpace(content.WriterName),
content.GetPropertyValue<DateTime?>("date") ?? content.CreateDate
));
}
And it passes! With a flying rainbow of colors even. (Actually just green)
We've managed to write logic against our content, test that it behaves correctly in both cases, and can now sleep soundly knowing that our editors can always override authors and dates. Note that the value of the stubbed properties has to be the same type as you request. Otherwise, you'll need to involve setting up property value converters.
There are several more techniques to unit testing Umbraco. I showed a few more in a talk on unit testing at CodeGarden 16. It is available for streaming here. The accompanying code and the code from this article is available on GitHub.
I strongly encourage you to poke around in the core unit tests to learn more about the helpers and utilities available for faking your Umbraco installation.
Have fun building rock solid software!
[Edit march 2017] Umbraco.Tests is now on Nuget, and I now recommend extracting Umbraco support into its own class. See The basic of unit testing Umbraco just got simpler.