Tuesday, April 23, 2019

Unit testing Sitecore index queries in 9.1

Unit testing Sitecore index queries

I will make a few assumptions in this post:
  • You know what unit testing is
  • You understand the benefits of unit testing
  • You are using Sitecore indexes in your code 
  • and so you would like to unit test that code 
If you don't understand the benefits yet.. consider this:
You have some code that needs to be tested with a number of variables (date in the past or in the future, positive or negative numbers ...). In order to test this properly you might need to create several items in Sitecore, run several manual tests and best of all - restart your Sitecore environment after every deploy with a bugfix if your code wasn't perfect from the start (and be honest, is that ever...)

Unit testing queries in 9.1

I have used code based on a solution by Vivian Roberts for a lot of projects to test code with queries on indexes. It works fine (as long as I don't have facets and such) and has saved me a bit of time already.

And then came Sitecore 9.1. And it didn't work anymore..  Sitecore introduced a new internal interface that is checked in the QueryableExtensions and that makes our test code fail:
if (!QueryableExtensions.IsIContentSearchQueryable<TSource>(source))
    return (SearchResults<TSource>) null;
The solution apparently was to inherit from Sitecore.ContentSearch.Linq.Parsing.GenericQueryable which is public and inherits the mentioned interface.  Thanks to my colleague Alex Dhaenens and Sitecore Support for helping us out on this one.

Let's recap with a complete overview of the code I'm using to test queries.

The code

Step 1: make the code testable

To make it all testable I create a SearchContextBuilder:
public class SearchContextBuilder : ISearchContextBuilder
{
  public virtual IProviderSearchContext GetSearchContext(string index)
  {
    return ContentSearchManager.GetIndex(index).CreateSearchContext();
  }
}
This builder will be the default that is injected in all our repositories that query indexes. In the test version however, we will be able to inject another one.

Generic Search repository

You could use the context builder in a generic search repository, something like:
public class SearchRepository : ISearchRepository
{
  private readonly ISearchContextBuilder searchContextBuilder;

  public SearchRepository(ISearchContextBuilder searchContextBuilder)
  {
    this.searchContextBuilder = searchContextBuilder;
  }

  public virtual SearchResults<T> GetResults<T>(Expression<Func<T, bool>> predicate, string index) where T : SearchResultItem
  {
    using (var context = searchContextBuilder.GetSearchContext(index))
    {
      var query = context.GetQueryable<T>().Where(predicate);
      return query.GetResults();
    }
  }

  public virtual IEnumerable<T> GetResultItems<T>(Expression<Func<T, bool>> predicate, string index) where T : SearchResultItem
  {
    var results = GetResults(predicate, index);
    foreach (var hit in results.Hits)
    {
      yield return hit.Document;
    }
  }
}

Step 2: create a custom QueryableCollection

This is the part where the code from Vivian Roberts is used. We did add the item count to make the result count work and adapted it to work with Sitecore 9.1. The QueryableCollection will be used in the test version of the search context.
public class SearchProviderQueryableCollection<TElement> : GenericQueryable<TElement,Query>, IOrderedQueryable<TElement>, IQueryProvider
{
  private readonly EnumerableQuery<TElement> innerQueryable;

  public SearchProviderQueryableCollection(IEnumerable<TElement> enumerable):base(null,null,null,null,null,null,null)
  {
    innerQueryable = new EnumerableQuery<TElement>(enumerable);
  }

  public SearchProviderQueryableCollection(Expression expression) : base(null, null, null, null, null, null, null)
  {
    innerQueryable = new EnumerableQuery<TElement>(expression);
  }

  public new Type ElementType => ((IQueryable)innerQueryable).ElementType;
  public new Expression Expression => ((IQueryable)innerQueryable).Expression;
  public new IQueryProvider Provider => this;

  public new IEnumerator<TElement> GetEnumerator()
  {
    return ((IEnumerable<TElement>)innerQueryable).GetEnumerator();
  }

  IEnumerator IEnumerable.GetEnumerator()
  {
    return GetEnumerator();
  }

  public IQueryable CreateQuery(Expression expression)
  {
    return new SearchProviderQueryableCollection<TElement>((IEnumerable<TElement>)((IQueryProvider)innerQueryable).CreateQuery(expression));
  }

  public new IQueryable<TElement1> CreateQuery<TElement1>(Expression expression)
  {
    return (IQueryable<TElement1>)new SearchProviderQueryableCollection<TElement>((IEnumerable<TElement>)((IQueryProvider)innerQueryable).CreateQuery(expression));
  }

  public object Execute(Expression expression)
  {
    throw new NotImplementedException();
  }

  public new TResult Execute<TResult>(Expression expression)
  {
    var items = this.ToList();
    object results = new SearchResults<TElement>(items.Select(s => new SearchHit<TElement>(0, s)), items.Count);
    return (TResult)results;
  }
}

Step 3: create our custom testable search builder

The testable search builder will be different for each type of tests you need as it will have the actual data to test against. Important is however the creation the context itself. Note that I am using Moq as mocking framework - you can use another if you want. An example could be:
public class TestableSearchBuilder : ISearchContextBuilder
{
  private readonly IList<SearchResultItem> items;

  public TestableSearchBuilder()
  {
    var templateId1 = new ID("{04fd3a5b-af21-49e3-9d88-25355301ab91}");
    var root = new ID("{0cbba84d-f2cd-4adb-912e-36d97cb22fe9}");
    var rootPath = new List<ID> { root };
    
    items = new List<SearchResultItem>
    {
      new SearchResultItem { Name = "Item1", Language = "en", ItemId = new ID(new Guid()), TemplateId = templateId1, Paths = rootPath },
      new SearchResultItem { Name = "Item2", Language = "nl", ItemId = new ID(new Guid()), TemplateId = templateId1, Paths = rootPath }
    };
  }

  public IProviderSearchContext GetSearchContext(string index)
  {
    // create the mock context
    var searchContext = new Mock<IProviderSearchContext>();
    var queryable = new SearchProviderQueryableCollection<SearchResultItem>(items);
    searchContext.Setup(x => x.GetQueryable<SearchResultItem>()).Returns(queryable);
    searchContext.Setup(x => x.GetQueryable<SearchResultItem>(It.IsAny<IExecutionContext>())).Returns(queryable);
    return searchContext.Object;
  }
}
Note that we are using our custom SearchProviderQueryableCollection in the context. The constructor is used to add data to the list of items that are in our fake index. You can put as many items in there as you need with all the available properties you need. For the example I used the SearchResultItem, but if you inherited from that class you can use yours here as well.

Step 4: start testing

We have everything in place now to start writing actual tests. I'm using Xunit for the tests as this one works fine with FakeDB, which I am sometimes using in combination with this code.

In the constructor of the test class (or setup method if not using Xunit) you need to create an instance of the TestableSearchBuilder. You need to get this instance in the class you are testing - see step 1 to make the code testable. So create an instance of the testable class (e.g. the SearchRepository) with the TestableSearchBuilder (new SearchRepository(TestableSearchBuilder)). Depending on your actual solution, you might need to tweak the IOC container as well to use these instances when needed in the tests.

And that's all..  you should be able to unit test queries and verify the results. 


No comments:

Post a Comment