Question

I have been using moles to write unit tests on the hard to reach parts of my code – particularly where I use the types found in the sitecore class libraries (which are hard to use mocking frameworks with). I have come across a surprisingly tricky problem where I’m trying to mole a LinkField type and test the following kind of code snippet.

LinkField linkField = item.Fields["External Url"];

if (linkField.IsInternal && linkField.TargetItem != null)
{
// want to test this path

item.Fields is a field collection whose indexer returns a type of SiteCore.Data.Field, but LinkField is set up such that uses an implicit operator which hides the conversion which means you are able to work with an instance of LinkField in your code.

My difficulty is that I cannot create a mole of type MLinkField, and assign this to the FieldCollection in the Item as it is strongly typed to Field. Moreover, it seems that whilst I am able to create a type of MField, when the implicit conversion happens, it will work and return an object but none of the fields are set to have any values. This means, I can’t test the code path above which relies on the state of the linkField being set a certain way.

The only way I can think of setting these values is indirectly – i.e. finding what values need to be set by analysing the implicit conversion and setting these in MField. The implicit operator calls the LinkField constructor as follows:

public LinkField(Field innerField) : base(innerField, "link")

which means I need to be conscious of how it instantiates the base type (XmlField), and in turn that class’s base type (CustomField). Then, to look at what underlying values TargetItem is looking for. The end result is the need to mole out:

InnerField.Database.Items[internalPath];

Or

InnerField.Database.Items[targetID];

Where InnerField is effectively my MField.

Does anyone have a better idea? This sounds horribly convoluted but I think that’s the nature of the beast with these assemblies.

Was it helpful?

Solution

It can be done but you'll need to jump through a few hoops to make it work.

First of all, a bit of background:

  • LinkField does not inherit from the Field class like some other field types do.
  • LinkField inherits from XmlField which in turn inherits from CustomField.
  • CustomField (and it's subtypes) are instantiated by passing a field instance into the constructor.
  • Linkfields store their values in this field instance.
  • Linkfields cannot be added to a FieldCollection as they do not inherit from Field.
  • Rather than add the LinkField we need to add the LinkField's InnerField property.
  • When a Field is pulled from a FieldCollection and assigned to a LinkField an implicit conversion operation is performed.
  • The implicit conversion operation creates a new LinkField by passing the selected field to the LinkField's constructor. It is this new LinkField that is returned.

Now some code:

const string externalUrl = "External Url";
const string targetItemName = "Target Item";                     
Field field = new ShimField { IDGet = () => ID.NewID, NameGet = () => externalUrl };
Item targetitem = new ShimItem { IDGet = () => ID.NewID, NameGet = () => targetItemName };
LinkField linkfield = new ShimLinkField(field) { IsInternalGet = () => true, TargetItemGet = () => targetitem };                         
ShimLinkField.ImplicitOpFieldLinkField = (f) => linkfield;
FieldCollection fields = new ShimFieldCollection { ItemGetString = (name) => linkfield.InnerField };
Item item = new ShimItem { NameGet = () => "Test Item", FieldsGet = () => fields };

And now for a bit of explanation:

The key to making the above code work is the line:

ShimLinkField.ImplicitOpFieldLinkField = (f) => linkfield;

By Shimming the implicit conversion operator, we can ensure that when the following line is called:

LinkField linkField = item.Fields["External Url"];  

A link field is returned and it's properties are shimmed as you want them.

OTHER TIPS

Since LinkField cannot be mocked in a proper way it have to be used in unit tests "as is" meaning that you'll have to test both your code and the link field implementation.

The good news is that the Link field is an XmlField that operates with the xml data stored in the inner field value. In order to configure the link field behavior you just need to set proper xml in the value.

With Sitecore.FakeDb you can easily create content in memory and configure items and fields you need. The following code creates items home and target. The home item has got Url link field which has TargetId pointed to the target item:

  ID targetId = ID.NewID;

  using (var db = new Db
    {
      new DbItem("home")
        {
          // Field 'Url' is an internal link which targets the 'target' item
          { "Url", "<link linktype=\"internal\" id=\"{0}\" />".FormatWith(targetId) }
        },
      new DbItem("target", targetId)
    })
  {
    Item item = db.GetItem("/sitecore/content/home");
    LinkField linkField = item.Fields["Url"];

    linkField.IsInternal.Should().BeTrue();
    linkField.TargetItem.Should().NotBeNull();
    linkField.TargetItem.Paths.FullPath.Should().Be("/sitecore/content/target");
  }

Personally speaking, I do try to abstract Sitecore away from business logic that doesn't need to interact with it directly; however I'm mindful that some things will need to deal with it directly and I try not to take it too far. If you spend too much time abstracting Sitecore out of your logic, then in my opinion you can end up making it more difficult to take advantage of some features that make Sitecore useful.

While unit testing purists may disagree with my approach, I will have unit tests that use the Sitecore API to properly instantiate items. Doing so is fairly straight-forward, I wrote a blog post about it a while ago. Then your only problem is dealing with test data, but it's fairly easy to create it with a test set-up and remove it with a test tear-down.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top