Wednesday, 26 March 2014

Unit testing Umbraco IPublishedContent with Microsoft Fakes

Update: with more recent versions of Umbraco, there is a way introduced to me by Lars-Erik and documented in his blog post here that allows for testing IPublished content as attempted in this now fairly old post, without the user of MS Fakes. Given the the MS Fakes product still requires the Ultimate version of VS.Net I'd recommend using the technique he describes.

I've recently been working on a package for Umbraco called Umbraco Mapper. It's intention is to support development of MVC based Umbraco applications by providing a simple means of mapping Umbraco content to custom view models. You can read more about it on the GitHub page.

For a few weeks now I've on and off tackled a problem I've found with unit testing part of the functionality. Umbraco content is represented in the front-end of the application by an interface IPublishedContent. What I'm looking to do is to create an instance of IPublishedContent with some known properties, map it to a custom view model class using a method of the mapper, and assert the results are as expected.

First failed attempt... using mocks

In this situation where I have a third party class to instantiate, the first tool I turn to is moq. This is a great little library for unit testing when you have an interface to work with - you can mock an instance that implements this interface and provide known values and functionality for properties and methods. Given those values, you can then action your test and assert the expected outcomes.

Here's a simple mock of IPublished content:

private static IPublishedContent MockIPublishedContent()
{
    var mock = new Mock<IPublishedContent>();
    mock.Setup(x => x.Id).Returns(1000);
    mock.Setup(x => x.Name).Returns("Test content");
    mock.Setup(x => x.CreatorName).Returns("A.N. Editor");
    return mock.Object;
}

And a test that uses that mock:

[TestMethod]
public void UmbracoMapper_MapFromIPublishedContent_MapsNativePropertiesWithMatchingNames()
{
    // Arrange
    var model = new SimpleViewModel();
    var content = MockIPublishedContent();
    var mapper = new UmbracoMapper();

    // Act
    mapper.Map(content, model);

    // Assert
    Assert.AreEqual(1000, model.Id);
    Assert.AreEqual("Test content", model.Name);
}

So far so good, but here I'm only testing properties of IPublishedContent. Of more value in the mapping tool is the mapping of the Umbraco document type properties, which are accessible via a method GetPropertyValue(alias). This makes calls various levels into the Umbraco code base, eventually leading to a null reference exception I believe due to missing a context of some sort.

Now I can add that method to the mock easily enough...

    mock.Setup(x => x.GetPropertyValue(It.IsAny<string>()))
      .Returns((string alias) => MockIPublishedContentProperty(alias));

... but unfortunately this doesn't work, as GetPropertyValue is an extension method that moq is unable to handle. The code compiles, but at runtime you'll get an error and this test will fail.

public void UmbracoMapper_MapFromIPublishedContent_MapsCustomPropertiesWithMatchingNames()
{
    // Arrange
    var model = new SimpleViewModel3();
    var content = MockIPublishedContent();
    var mapper = new UmbracoMapper();

    // Act
    mapper.Map(content, model);

    // Assert
    Assert.AreEqual("This is the body text", model.BodyText);
}

Second failed attempt... using stubs

My next attempt was to create my own class, StubPublishedContent that implements the IPublishedContent interface. Instead of mocking the properties and methods I created stub instantiations of them to just return constant values or simple variations based on inputs.

Of course as I found above, GetPropertyValue(alias) isn't defined on the IPublishedContent interface - but I figured if I just create a method with that signature, given instance methods take precedence over extension methods with the same signature, maybe it'll use my implementation at runtime? Well, no. Confused me for a bit but the citizens of stackoverflow set me straight.

And success... using Microsoft's Fakes

Microsoft Fakes offers a means of replacing certain method calls within any referenced assembly at runtime. To do this you find the assembly that your method is in - in my case umbraco.dll - right-click on the reference and select Add Fakes Assembly.

Once that's complete I could re-write my failing test above like this:

[TestMethod]
public void UmbracoMapper_MapFromIPublishedContent_MapsCustomPropertiesWithMatchingNames()
{
    // Using a shim of umbraco.dll
    using (ShimsContext.Create())
    {
        // Arrange
        var model = new SimpleViewModel3();
        var mapper = new UmbracoMapper();
        var content = new StubPublishedContent();

        // - shim GetPropertyValue (an extension method on IPublishedContent in Umbraco.Web.PublishedContentExtensions)
        Umbraco.Web.Fakes.ShimPublishedContentExtensions.GetPropertyValueIPublishedContentStringBoolean =
            (doc, alias, recursive) =>
            {
                switch (alias)
                {
                    case "bodyText":
                        return "This is the body text";
                    default:
                        return string.Empty;
                }                        
            };

        // Act
        mapper.Map(content, model);

        // Assert
        Assert.AreEqual(1000, model.Id);
        Assert.AreEqual("Test content", model.Name);
        Assert.AreEqual("This is the body text", model.BodyText);
    }
}

I'm replacing the call to GetPropertyValue(alias, recursive) at runtime with my own function, that uses a simple switch statement to return the appropriate document type property value. And at last... a green test!

1 comment: