0

Problem

Is there a way to mock Template.InheritsFrom using NSubstitute?

This is needed in order to be able to test logic that depends on the presence of a base template in the inheritance chain.

In application code, one might write:

if (item.Template.InheritsFrom(specificBaseTemplateID))

But it appears to be impossible to cover this direclty with a unit test. Although one can create a mock Sitecore item (see this or this, any attempt to access the item.Template field of that item mock will fail.

And attempting to create a mock template, as proposed in the answer below

var template = Substitute.For<Template>();

will result in this error:

System.NotSupportedException
Parent does not have a default constructor. The default constructor 
must be explicitly defined.

NSubstitute allows passing values to a constructor, which works for Item (see links above), but will not work for Template, becuse it has a private constructor, which the test code can't access:

 private Template(
      string templateName,
      ID templateID,
      TemplateRetrievalFacade templateParentStorage,
      BaseSettings settings)
    { ... }

So, per my analysis, there is no way using tools like NSubsitute to mock a template, or to mock the result of item.Template, the way you can for item.ID or item.TemplateId. The member item.Template is virtual, but it returns a type that cannot be created in a unit test, so it's being virtual is useless.

Workaround

We can create a wrapper class to encapsulate this behavior:

public class InheritanceChecker: IInheritanceChecker {
       
 BaseTemplateManager _templateManager;
 
 public InheritanceChecker(BaseTemplateManager templateManager) {
   _templateManager = templateManager;
 }

 public bool ItemInheritsFrom(Item item, ID templateId) => 
   _templateManager.GetTemplate(item).InheritsFrom(templateId);

}

public Interface IInheritanceChecker {
  bool ItemInheritsFrom(Item item, ID templateId);
}

After this class and template is registered with the Sitecore Dependency Injection container, you will be able to write testable business logic, as this example shows:

 IInheritanceChecker inheritanceChecker = Substitute.For<IInheritanceChecker>();
 inheritanceChecker.ItemInheritsFrom(item, templateId).Returns(true);

However, this requires adding a custom class to application logic which provides no value except testability. It's an unfortunate hole in the generally high level of testabilty added by the addition of the abstraction layer with 8.2.

Possible Sitecore Fixes

I will follow up with Sitecore support to extend BaseTemplateManager to provide this method:

abstract bool ItemInheritsFrom(Item item, ID baseTemplateId);

DefaultTemplateManager could implement like this:

bool ItemInheritsFrom(Item item, ID templateId) => 
   this.Provider.GetTemplate(item).InheritsFrom(baseTemplateId);

A more robust fix, making Template abstract, and having a TemplateImplementation class that holds the private constructor, woudl be preferrable but would be a much bigger lift, though probably not a breaking change for implementers. I'll propose both to Sitecore, assuming my understanding of the underlying issue is correct.

Summary

This is to confirm my analysis that Sitecore.Data.Templates.Template is not mockable. If that's correct, I'll use the workaround above, and open a feature request with Sitecore.

Ideally, someone in the community with extensive Sitecore unit testing experience can either validate my analysis or point out what I missed (making the workaround and feature request unncessary).

2
  • Note to reviewer: I'm pretty confident in my analysis, but wanted a second pair of eyes from the community from anyone who knows unit testing well (e.g. sitecore.stackexchange.com/users/272/serhii-shushliapin). If preferred, I can pull my workaround out of the question, and put it in an answer. But I was really looking for confirmation, since this is a really surprising gap in Sitecore's abstraction layer. Commented Nov 30, 2023 at 17:26
  • Update: The correct pattern is to mock item.DescendsFrom(templateID) which is virtual and does not depend on instantiating a Template item. So no need for API changes or wrapper classes. If this question is unblocked, I'll answer with a full code sample. Commented Dec 1, 2023 at 2:17

1 Answer 1

1

I think the most important question here is "Why do you even want to write a test checking if item inherits from baseTemplateId" Do you want to write unit tests for your code or for Sitecore code?

You can mock Template.InheritsFrom using:

var templateA = ID.NewID;
var templateB = ID.NewID;

var template = Substitute.For<Template>();
template.InheritsFrom(templateA).Returns(true);
template.InheritsFrom(templateB).Returns(false);

var templateManager = Substitute.For<BaseTemplateManager>();

templateManager.GetTemplate(item).Returns(template);

Assert.True(templateManager.GetTemplate(item).InheritsFrom(templateA));
Assert.False(templateManager.GetTemplate(item).InheritsFrom(templateB));

But if you do that, you will not test if item inherits or not from given template - it will just mock the response.

I think there is something that you want to achieve but you haven't included it in your question. Maybe elaborate more so the community can help you better.

3
  • Your code won't work, because Template has a private default constructor. Line 3 will get this error: "Parent does not have a default constructor. The default constructor must be explicitly defined." That was the point of my question, but I agree I muddied the waters by discussing possible Sitecore API changes. I'll create a second question in Q&A format to docuemnt the workaround. Commented Nov 30, 2023 at 15:44
  • To answer your question, it's completely normal to want to cover logic that is conditioned on the presence of a base template in the inheritance chain. It's testing "my" logic, which depends on a certain fact being true with the Sitecore content it's interacting with. To do that, you need to be able to create a stub item that satisfies "inherits from" and one that doesn't, which means you need to be able to control the results of this method, or another method that wraps it. Commented Nov 30, 2023 at 15:49
  • I've updated my question to clarify that the NSubtitute call fails, and to clarify the business case for mocking this. Commented Nov 30, 2023 at 17:28

Not the answer you're looking for? Browse other questions tagged or ask your own question.