Life Through A Lens – Unit Testing your .net code using Glass Mapper

So now I have gotten my first couple of Sitecore User Group presentations out of the way – I thought I would actually continue on my journey of showing how I use Glass Mapper on the Sitecore platform in real world application.

As you will see from other posts I write – I am very much a TDD advocate. I find it helps focus my mind much better than simply writing code ad-hoc. Also – I hate testing, but love coding – so what better way to get my testing done than coding.

In choosing an ORM, I believe this should be one of your primary criteria since you are going to take some sort of performance hit with it being in the way and for me, trying to justify as just being ‘prettier’ code doesn’t really cut it when you are presenting it to a wider team.

With this in mind, I thought I would kinda tie these two together and look at how I use Glass to perform unit testing on *MY* code.

If you are new to unit testing – I would definitely recommend you check out Simon’s post on unit testing and if you have time my post on Unit Testing on Sitecore Pt I and Unit Testing On Sitecore Pt II. These I think give a good grounding in how I approach unit testing and then in particular unit testing on the Sitecore platform.

As described in the posts above, the same is true in that I actually try to actively NOT test Glass, it has over 700 unit tests (at time of writing) in the code base and I am going to try and write more for Glass functionality I have developed when time allows.

The Sitecore Service / Context

This should be your starting point for any unit tests for which you want to use Glass. I find more often than not, if I am intending to interface with Sitecore, I am usually starting with a constructor parameter for ISitecoreService or ISitecoreContext. Given that these are both interfaces they are easily mockable and unlike some other modules I have seen and worked with, the level of mocking required has been kept to a minimum.

As a quick example – the following code would be all that’s required to mock a return from the most basic Sitecore service return.

            ISitecoreService glassService = Substitute.For<ISitecoreService>();
            glassService.GetItem<JobHub>(SitecoreIds.JobHubId.ToGuid()).Returns(new JobHub());

Mocking, interfaces and concrete

One of the great features Glass Mapper has is that it can create either concrete OR interface returns. For the most part we tend to use interfaces but this presents challenges both for unit testing and for active use.

public interface IBox
{
   int Width { get; set; }
   
   int Height { get; set; }

   int Area { get; set; }

   IDecoration Decoration { get; set; }
}

When we consider this in the context of providing this in a service return we kinda left with a crux point. In order for Area to be complete and correct, ideally we simply want to multiply width x height. Unfortunately with an interface this isn’t so easy. With a concrete object we could simply have the result of Area calculated as part of a method or directly in the property. Of course you can use the Delegate mapper (which should soon be in Glass Mapper natively) to help you, OR you can use an extension method (which is annoying to unit test given it’s static declaration).

Furthermore, when it comes to making the type above behave for unit tests, we are faced with a simple issue in filling in the Decoration property. We are required to not only mock the IBox interface, but also mock the return for the Decoration property using yet another mock.

Concrete objects are not without their problems either, obviously the issues with structure inflexibility due to the single inheritance heirarchy can rear it’s ugly head and often makes code unwieldy.

Of course, you can mix these, and this has generally been my approach in the past. Knowing when to do so in my humble opinion is a matter of trying it out and figuring out what level of code inflexibility you are prepared to put up with from both sides. There is unfortunately no holy grail. That said, the fixes that have been recently implemented in Glass Mapper to allow you to import types a little more easily will make life much easier in the future.

Casting items to objects

I thought I would just say a few lines on this since it’s actually surprisingly common to have to perform this action and therefore often used in our test cases.

For the most part, we can deal solely with Glass as our ORM. There will however be several occasions (particularly when dealing with pipelines / commands / sheer ui) that you will be presented with like an args.Item or something and be expected to (hopefully) then deal with this using Glass Mapper.

FIRSTLY, let me say this – you CAN use dependency injection in pipelines / commands. You have 2 choices as I see it.

1) is the service locator (anti) pattern. I would advise against this.
2) is the use of Sitecore Factory objects, which I described in this post

Glass DOES have a GlassCast() extension method… DON’T use it! It has to create an instance of a sitecore service every time you use it in order to return an object. 99% of the time, we have access to one from our IoC container.

INSTEAD (until the Glass team accept one of my features which will give you Cast(Item item)), you should use the CreateType(Item item) method on the Sitecore service. This will return your input item as the mapped type. If you have already been using Glass and fallen into this trap – don’t worry, loads of us have. Longer term I expect GlassCast will either be deprecated or require a service as an input parameter (watch this space 😉 ).

Listing

Ok, so – SHOW ME THE CODE!!

The following sample is from a wider demo Life Through A Lens that I have ran on using Glass Mapper. It shows in practice how we might go about testing a careers page that is to only show us jobs where the deadline date is in the future. As you will see reading further, it then goes on to look at varying success and failure scenario’s, but I am sure you will agree that it is about as close to plain old .net code as you are likely to see on the Sitecore platform.


using System;
using System.Collections.Generic;
using System.Linq;
using Glass.Mapper.Sc;
using LifeThroughALens.Domain;
using NSubstitute;
using NUnit.Framework;

namespace LifeThroughALens.UnitTests
{
    [TestFixture]
    public class JobServiceTests
    {
        [Test]
        public void CanGetActiveJobsSuccessfully()
        {
            // Assign
            ISitecoreService glassService = Substitute.For<ISitecoreService>();
            JobHub jobHub = new JobHub();
            var job1 = Substitute.For<IJob>();
            job1.DeadlineDateTime = DateTime.Today.AddDays(-1);
            var job2 = Substitute.For<IJob>();
            job2.DeadlineDateTime = DateTime.Today.AddDays(1);
            IEnumerable<IJob> jobs = new[] {job1, job2};
            ILog log = Substitute.For<ILog>();

            jobHub.Jobs = jobs;
            glassService.GetItem<JobHub>(SitecoreIds.JobHubId.ToGuid()).Returns(jobHub);
            JobsService jobsService = new JobsService(glassService, log);

            // Act
            var result = jobsService.GetLiveJobs().ToArray();

            // Assert
            glassService.Received(1).GetItem<JobHub>(SitecoreIds.JobHubId.ToGuid());
            log.Received(0).LogException(Arg.Any<string>(), Arg.Any<Exception>());
            Assert.AreEqual(1, result.Length);
            Assert.AreEqual(job2, result.FirstOrDefault());
        }

        [Test]
        public void CanGetActiveJobsNoActiveJobs()
        {
            // Assign
            ISitecoreService glassService = Substitute.For<ISitecoreService>();
            JobHub jobHub = new JobHub();
            ILog log = Substitute.For<ILog>();

            glassService.GetItem<JobHub>(SitecoreIds.JobHubId.ToGuid()).Returns(jobHub);
            JobsService jobsService = new JobsService(glassService, log);

            // Act
            var result = jobsService.GetLiveJobs();

            // Assert
            glassService.Received(1).GetItem<JobHub>(SitecoreIds.JobHubId.ToGuid());
            log.Received(0).LogException(Arg.Any<string>(), Arg.Any<Exception>());
            Assert.IsNull(result);
        }

        [Test]
        public void CanGetActiveJobsServiceException()
        {
            // Assign
            ISitecoreService glassService = Substitute.For<ISitecoreService>();
            ILog log = Substitute.For<ILog>();

            glassService.GetItem<JobHub>(SitecoreIds.JobHubId.ToGuid()).Throws<Exception>();
            JobsService jobsService = new JobsService(glassService, log);

            // Act
            var result = jobsService.GetLiveJobs();

            // Assert
            glassService.Received(1).GetItem<JobHub>(SitecoreIds.JobHubId.ToGuid());
            log.Received(1).LogException(Arg.Any<string>(), Arg.Any<Exception>());
            Assert.IsNull(result);
        }

        [Test]
        public void CanGetActiveJobsServiceNullReturn()
        {
            // Assign
            ISitecoreService glassService = Substitute.For<ISitecoreService>();
            ILog log = Substitute.For<ILog>();

            glassService.GetItem<JobHub>(SitecoreIds.JobHubId.ToGuid()).Returns(null as JobHub);
            JobsService jobsService = new JobsService(glassService, log);

            // Act
            var result = jobsService.GetLiveJobs();

            // Assert
            glassService.Received(1).GetItem<JobHub>(SitecoreIds.JobHubId.ToGuid());
            log.Received(0).LogException(Arg.Any<string>(), Arg.Any<Exception>());
            Assert.IsNull(result);
        }
    }
}

Conclusion

As you can see from above, the act of unit testing using Glass Mapper is incredibly simple. In future posts I will cover off unit testing your controllers using a similar technique.

Additional Reading

  • Life Through A Lens Series
  • Testing
    • Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s