Tuesday 8 November 2016

Unit testing GlassController methods and Sitecore rendering parameters

I finally had some time to work on getting some unit tests set up for the project I'm working on, which is using Glass Mapper (handy in pre-Habitat projects).  I'd done some testing using Glass Mapper before (using FakeDb, which is awesome), but wanted to take things in a Habitat-style direction (xUnit, NSubstitute) and also hadn't done anything around testing rendering parameters.

Yes, you can stub ISitecoreContext and tell it what to return when your controllers use it (to grab datasources and items), but I wanted to really streamline as much as possible and not change any of my controller's uses of GetDatasourceItem<IMyInterface>() and GetRenderingParameters<IMyInterface>() - after all why not make use of GlassController and these methods if you're using Glass?

Glass gives us a nice way to stub datasource and rendering strings out through the RenderingContextWrapper property of GlassController (which is an IRenderingContext).  By creating a mock class we can pass through any datasource and rendering parameters (as a query string).

public class MockRenderingContext : IRenderingContext
{
    private readonly string _datasourceId;

    private readonly string _renderingParams;

    public MockRenderingContext(string datasourceId, string renderingParams)
    {
        _datasourceId = datasourceId;
        _renderingParams = renderingParams;
    }

    public string GetRenderingParameters()
    {
        return _renderingParams;
    }

    public string GetDataSource()
    {
        return _datasourceId;
    }

    public bool HasDataSource => !string.IsNullOrEmpty(_datasourceId);
}

We can assign this easily to our controller (which inherits from GlassController) in our test class:

ControllerToTest controller = new ControllerToTest(sitecoreContext)
{
    RenderingContextWrapper = new MockRenderingContext("/sitecore/content/mydatasource", "RenderingParam=1234")
};

All good so far.  This lets us grab the datasource, as it uses the SitecoreContext.GetItem<T>(datasource) to grab the relevant item.  Not so for the rendering parameters, unfortunately...  This calls GlassHtml.GetRenderingParameters<T>(renderingParametersString)!

So, what's the problem? I hear you ask.

Well, digging into what happens next (in GlassHtml): the rendering parameters string is split into KeyValuePairs, the template ID is retrieved from the Glass cache, and it ends up at:

public T GetRenderingParameters<T>(NameValueCollection parameters, ID renderParametersTemplateId) where T : class
{
    var item = Utilities.CreateFakeItem(null, renderParametersTemplateId, SitecoreContext.Database, "renderingParameters");

    using (new SecurityDisabler())
    {
        using (new EventDisabler())
        {
            using (new VersionCountDisabler())
            {
                item.Editing.BeginEdit();

                foreach (var key in parameters.AllKeys)
                {
                    item[key] = parameters[key];
                }

                T obj = SitecoreContext.Cast(item);

                item.Editing.EndEdit();
                item.Delete(); //added for clean up
                return obj;
            }
        }
    }
}


Now apparently because we're using FakeDb, the item that is dynamically built (to be cast into our parameters model) is not populated correctly!  So, let's override the appropriate method and do it the FakeDb way:

public class FakeGlassHtml : GlassHtml
{
    private readonly Db _db;

    public FakeGlassHtml(ISitecoreContext context, Db db) : base(context)
    {
        _db = db;
    }

    /// <summary>
    /// Method overridden to call our new method (below)
    /// </summary>
    /// <typeparam name="T">Type of rendering parameters to create</typeparam>
    /// <param name="parameters">Name/value collection of parameters</param>
    /// <returns>Populated parameters object</returns>
    public override T GetRenderingParameters<T>(NameValueCollection parameters)
    {
        if (parameters == null)
        {
            return default(T);
        }

        var config = SitecoreContext.GlassContext[typeof(T)] as SitecoreTypeConfiguration;

        if (config == null)
        {
            SitecoreContext.GlassContext.Load(new OnDemandLoader<SitecoreTypeConfiguration>(typeof(T)));
        }

        config = SitecoreContext.GlassContext[typeof(T)] as SitecoreTypeConfiguration;

        return GetRenderingParameters<T>(parameters, config?.TemplateId);
    }

    /// <summary>
    /// New method using FakeDB to create rendering parameters in the same way as the original Glass Mapper code
    /// </summary>
    /// <typeparam name="T">Type of rendering parameters to create</typeparam>
    /// <param name="parameters">Name/value collection of the parameters</param>
    /// <param name="renderParametersTemplateId">Rendering parameters template ID</param>
    /// <returns>Populated parameters object</returns>
    public new T GetRenderingParameters<T>(NameValueCollection parameters, ID renderParametersTemplateId) where T : class
    {
        if (renderParametersTemplateId.IsNull)
        {
            return default(T);
        }

        ID newId = ID.NewID;
        DbItem tempItem = new DbItem("temp", newId, renderParametersTemplateId);
        
        foreach (string key in parameters.AllKeys)
        {
            tempItem.Fields.Add(new DbField(key) { Value = parameters[key] });
        }

        _db.Add(tempItem);
        
        Item item = _db.GetItem(newId);

        T obj = SitecoreContext.Cast<T>(item);

        // Clean up
        item.Delete();
        (_db as CirrusDb)?.Remove(newId);

        return obj;
    }
}

Those of you with a keen eye might have noticed mention of DbWithRemove at the bottom there.  Seems that FakeDb has no remove method, even though its DataStorage class inside does.  I've just created a simple class to enable us to clean up our temporary item as per the original Glass code.

public class CirrusDb : Db
{
    public void Remove(ID itemId)
    {
        DataStorage.RemoveFakeItem(itemId);
    }
}

So now putting it all together in our test method:

using (Db db = SetupTestsite())
{
    using (new SiteContextSwitcher(FakeSite()))
    {
        ISitecoreContext sitecoreContext = new SitecoreContext(db.Database);
        
        ComponentsController controller = new ComponentsController(sitecoreContext)
        {
            RenderingContextWrapper = new MockRenderingContext(datasourceId, renderingParameterQueryString),
            GlassHtml = new FakeGlassHtml(sitecoreContext, db)
        };

        ViewResult result = controller.MyComponentMethod() as ViewResult;

        // Check your view and model

        Assert.NotNull(result);
        Assert.Equal("~/Views/Components/MyComponent.cshtml", result.ViewName);
        IMyModel model = result.ViewData.Model as IMyModel;
        Assert.NotNull(model);
        // ... etc.
    }
}

Hopefully some time soon I'll get to work on a Sitecore 8.2 project and get to try out testing with some of the new interfaces!