Go to main content Go to main menu

How to unit-test extension methods

By Linus Ekström Reading time: 2 minutes

A good coding practice is to keep the view layer in an MVC structure as simple as possible and with no or minimal logic. In this blog post I’ll go through a pattern that I often use to be able to create easy to use extension methods – but without sacrificing the testability.

A good coding practice is to keep the view layer in an MVC structure as simple as possible and with no or minimal logic. A common practice to extract common logic that you might want to use in many places is to create an extension method that could be used across views. This moves to logic from the views into a C#-based method. However, I often see that these extension methods are often not properly unit tested – even though they often contain quite some logic which makes them a very good case for some Test Driven Development. In this blog post I’ll go through a pattern that I use to both make it easy to use the method in the view – but without sacrificing the testability.

An example of a non testable implementation

This is probably the most common implementation pattern that I see when I look at implementations of extension methods:

public static MvcHtmlString GetTagCloud(this ContentPageBase page)
{
  var taxonomyLoader = ServiceLocator.Current.GetInstance<ITaxonomyLoader>();
  var urlResolver = ServiceLocator.Current.GetInstance<IUrlResolver>();

  var tagCloudString = //Implementation to create tag cloud string goes here

  return tagCloudString;
}

Summary:

  • A simple method with no additional parameters.
  • Fetch dependencies from IOC container.
  • Do logic in method.
  • Return the results.
  • Not possible to test without working directly against the IOC container.

Refactoring the code to allow for better testability

Let’s do some refactoring to make this easier to test by letting the simple to use method be responsible for resolving the dependencies, and then calling another overload that handles the actual logic:

public static MvcHtmlString GetTagCloud(this ContentPageBase page)
{
  var taxonomyLoader = ServiceLocator.Current.GetInstance<ITaxonomyLoader>();
  var urlResolver = ServiceLocator.Current.GetInstance<IUrlResolver>();
  var taxonomyTypes = TaxonomyTypes.All;

  return GetTagCloud(page, taxonomyTypes, taxonomyLoader, urlResolver);
}

public static MvcHtmlString GetTagCloud(
            this ContentPageBase page,
            TaxonomyTypes taxonomyTypes,
            ITaxonomyLoader taxonomyLoader,
            IUrlResolver urlResolver)
{
  var tagCloudString = //Implementation to create tag cloud string goes here

  return tagCloudString;
}

Applying unit testing

Once the extension method has been refactored to support sending in dependencies you can start writing your unit tests. Not only does this ensure that you both test and get a “contract” for how the functionality should work – but it’s also a lot quicker to develop compared to the pattern where you need to find a suitable page to test on. In the example below we see a unit test class with set up and a unit test (I’ve removed additional tests for brevity).

namespace Episervercom.SiteTests.Areas.Shared.HtmlHelpers
{
    public class TaxonomyExtensionsTests
    {
        private readonly Mock<ITaxonomyLoader> _taxonomyLoaderMock;
        private readonly Mock<IUrlResolver> _urlResolverMock;
        private List<ITopicTaxonomyItem> _topics;
        private List<IIndustryTaxonomyItem> _industries;

        public TaxonomyExtensionsTests()
        {
            _taxonomyLoaderMock = new Mock<ITaxonomyLoader>();
            _urlResolverMock = new Mock<IUrlResolver>();

            _urlResolverMock.Setup(m => m.GetUrl(It.Is<ContentReference>(c => c.ID == 1), null, null)).Returns("/somecontenttype");
            _urlResolverMock.Setup(m => m.GetUrl(It.Is<ContentReference>(c => c.ID == 123), null, null)).Returns("/page123");
            _urlResolverMock.Setup(m => m.GetUrl(It.Is<ContentReference>(c => c.ID == 234), null, null)).Returns("/page234");
            _urlResolverMock.Setup(m => m.GetUrl(It.Is<ContentReference>(c => c.ID == 345), null, null)).Returns("/page345");
            _urlResolverMock.Setup(m => m.GetUrl(It.Is<ContentReference>(c => c.ID == 20), null, null)).Returns("/industry2");


            var contentTypes = new List<IContentTypeTaxonomyItem>()
            {
                new MockContentTypeTaxonomyItem("Content type 1", 1),
                new MockContentTypeTaxonomyItem("Content type 2", 2)
            };

            _taxonomyLoaderMock.Setup(m => m.GetContentTypes()).Returns(contentTypes);

            _topics = new List<ITopicTaxonomyItem>()
            {
                new MockTopic("tag1", 123),
                new MockTopic("tag2", 234),
                new MockTopic("tag3", 345)
            };

            _taxonomyLoaderMock.Setup(m => m.GetTopics()).Returns(_topics);

            _industries = new List<IIndustryTaxonomyItem>()
            {
                new MockIndustry("Industry 1", 10),
                new MockIndustry("Industry 2", 20)
            };

            _taxonomyLoaderMock.Setup(m => m.GetIndustries()).Returns(_industries);
        }

        [Fact]
        public void GetTagCloudTests()
        {
            var page = CreateTestPage();

            var result = page.GetTagCloud(TaxonomyTypes.All, _taxonomyLoaderMock.Object, _urlResolverMock.Object);

            Assert.Equal("<ul class=\"tag-cloud\"><li><a href=\"/somecontenttype\">Content type 1</a></li><li><a href=\"/page123\">tag1</a></li><li><a href=\"/page345\">tag3</a></li><li><a href=\"/industry2\">Industry 2</a></li></ul>", result.ToHtmlString());
        }

        private MockTeasableContentPage CreateTestPage()
        {
            var page = new MockContentPage();
            page.Topics = new List<ContentReference> { _topics[0].ContentLink, _topics[2].ContentLink };
            page.Industry = _industries[1].ContentLink;
            return page;
        }
    }
}

 

Summary

With a rather simple refactoring we are now able to create unit tests and if wanted also applying TDD style coding for your extension methods. Another positive side effect of this is that you also get a better visibility of the dependencies by lifting them out from the method that holds the actual implementation. Though the unit test class contains a bit of set up for the first test - adding new tests is really quick once this is done.

 

We would like to hear what you think about the blog post

0
    Linus Ekström
    Linus Ekström

    CEO | Solution Architect | OMVP

    Read all blog posts by Linus Ekström