Five New Optimizely Certifications are Here! Validate your expertise and advance your career with our latest certification exams. Click here to find out more

Henrik Fransas
Jan 22, 2015
  2995
(0 votes)

How to do unit testing on EPiServer Find

Changes in the web project

I was interested in if it was possible to do unit testing for a EPiServer Find implementation so I created a new Alloy site and added EPiServer Find to it. After that I wrote an implementation that did exactly the things EPiServer Search does in Alloy, and that is to search on all, pages and media objects.

To do that I wrote a SearchService that looks like this:

using System.Collections.Generic; using EPiServerFindTestSite.Models.ViewModels; namespace EPiServerFindTestSite.Business { public interface IEPiServerFindSearchService { IEnumerable<SearchContentModel.SearchHit> SearchFind(string searchText, int maxResults); } }
using System.Collections.Generic; using System.Linq; using EPiServer.Find; using EPiServer.Find.Statistics; using EPiServer.Find.UnifiedSearch; using EPiServerFindTestSite.Models.ViewModels; namespace EPiServerFindTestSite.Business { public class EPiServerFindSearchService : IEPiServerFindSearchService { private readonly IClient _client; public EPiServerFindSearchService(IClient iClient) { _client = iClient; } public IEnumerable<SearchContentModel.SearchHit> SearchFind(string searchText, int maxResults) { var query = _client.UnifiedSearchFor(searchText); var searchResults = query.Take(maxResults).Track().GetResult(); return searchResults.Hits.SelectMany(s => CreateEPiServerFindHitModel(s.Document)); } private IEnumerable<SearchContentModel.SearchHit> CreateEPiServerFindHitModel(UnifiedSearchHit hit) { yield return CreatePageHitFromFind(hit); } private SearchContentModel.SearchHit CreatePageHitFromFind(UnifiedSearchHit hit) { return new SearchContentModel.SearchHit { Title = hit.Title, Url = hit.Url, Excerpt = hit.Excerpt }; } } }

As you can see that is not much logic at all in it, it is just doing a unifiedsearch for a specific string and gives the possibility to add how many items to return. I am using a Interface for the service and also taking in the Client as a parameter to the constructor to be able to add tests to it.

After that I rewrite the existing SearchPageController so it was using the new SearchService and it now looks like this:

using System.Linq; using System.Web.Mvc; using EPiServerFindTestSite.Business; using EPiServerFindTestSite.Models.Pages; using EPiServerFindTestSite.Models.ViewModels; namespace EPiServerFindTestSite.Controllers { public class SearchPageController : PageControllerBase<SearchPage> { private readonly IEPiServerFindSearchService _ePiServerFindSearchService; public SearchPageController(IEPiServerFindSearchService ePiServerFindSearchService) { _ePiServerFindSearchService = ePiServerFindSearchService; } [ValidateInput(false)] public ViewResult Index(SearchPage currentPage, string q) { const int maxResults = 40; var model = new SearchContentModel(currentPage) { SearchServiceDisabled = false, SearchedQuery = q }; if (!string.IsNullOrWhiteSpace(q)) { var hits = _ePiServerFindSearchService.SearchFind(q.Trim(), maxResults).ToList(); model.Hits = hits; model.NumberOfHits = hits.Count(); } return View(model); } } }

As you can see it is much smaller than the original one and I have also moved the logic for creating the seachhits to the SearchService.

To make it work in the web I had to add this in the file DependencyResolverInitialization.cs in the function ConfigureContainer

container.Scan(c => { c.AssemblyContainingType<IEPiServerFindSearchService>(); c.WithDefaultConventions(); });

The reason I had to do that is that it is needed to help StructureMap understand how it should handle the controller.

 

Test projects

I ended up creating two different test projects with one test in each project. This is absolutely not something that you should do, I had to do it because I started with a project with nUnit and when I tried to Mock EPiServer Find I could not do it with FakeItEasy because that does not support mocking extension methods. Therefore I created a new MSTest project where I was able to use Fakes (only available in Premium and Ultimate version of VS2013) but I kept the other project because it shows how to do more simple tests with nUnit and FakeItEasy.

The first test I did was a test for the SearchPageController and this I did with nUnit and FakeItEasy. To be able to execute an action I created a helper class that looks like this:

using System; using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; using System.Web.Mvc; using System.Web.Routing; namespace EPiServerFindTestSite.Test { // http://www.codeproject.com/Articles/623793/OnActionExecuting-and-OnActionExecuted-in-MVC-unit#_rating public static class ControllerHelper { public static T ExecuteAction<T>(Expression<Func<T>> exp) where T : ActionResult { var methodCall = (MethodCallExpression)exp.Body; var method = methodCall.Method; var memberExpression = (MemberExpression)methodCall.Object; Expression<Func<Object>> getCallerExpression = Expression<Func<Object>>.Lambda<Func<Object>>(memberExpression); Func<Object> getCaller = getCallerExpression.Compile(); var ctrlr = (Controller)getCaller(); ControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(ctrlr.GetType()); ActionDescriptor actionDescriptor = new ReflectedActionDescriptor(method, method.Name, controllerDescriptor); // OnActionExecuting var rc = new RequestContext(); ctrlr.ControllerContext = new ControllerContext(rc, ctrlr); var ctx1 = new ActionExecutingContext(ctrlr.ControllerContext, actionDescriptor, new Dictionary<string, object>()); MethodInfo onActionExecuting = ctrlr.GetType().GetMethod( "OnActionExecuting", BindingFlags.Instance | BindingFlags.NonPublic); onActionExecuting.Invoke(ctrlr, new object[] { ctx1 }); // call controller method T result = exp.Compile()(); // OnActionExecuted var ctx2 = new ActionExecutedContext(ctrlr.ControllerContext, actionDescriptor, false, null) { Result = result }; MethodInfo onActionExecuted = ctrlr.GetType().GetMethod( "OnActionExecuted", BindingFlags.Instance | BindingFlags.NonPublic); onActionExecuted.Invoke(ctrlr, new object[] { ctx2 }); return (T)ctx2.Result; } } }

After that I wrote a very very simple test of the controller that looks like this:

using System.Collections.Generic; using System.Linq; using EPiServerFindTestSite.Business; using EPiServerFindTestSite.Controllers; using EPiServerFindTestSite.Models.Pages; using EPiServerFindTestSite.Models.ViewModels; using NUnit.Framework; using FakeItEasy; namespace EPiServerFindTestSite.Test { [TestFixture] public class EPiServerFindTests { [Test] public void SearchPageController_ShouldReturn_SearchHits() { var ePiServerFindSearchService = A.Fake<IEPiServerFindSearchService>(); var controller = new SearchPageController(ePiServerFindSearchService); var hits = new List<SearchContentModel.SearchHit> { new SearchContentModel.SearchHit() { Excerpt = "Excerpt", Title = "Title", Url = "Url" } }; A.CallTo(() => ePiServerFindSearchService.SearchFind("searchString", 40)).Returns(hits); var result = ControllerHelper.ExecuteAction(() => controller.Index(new SearchPage(), "searchString" )); var model = ((SearchContentModel)result.Model); Assert.That(model.NumberOfHits, Is.EqualTo(1)); Assert.That(model.Hits.First().Title, Is.EqualTo("Title")); } } }

This test starts with creating a Fake of IEPiServerFindSearchService that I use when creating the Controller. After that I create the list of hits that I want to get back from the SearchService and that list I use when telling FakeItEasy that for function SearchFind with the parameters “searchString” and 40 you should return this list (40 is the default value created in the controller). After that I just call the action index with a new SearchPage and the string “searchString” and compare the result with the expected result.

For the other test that actually test the SearchService it proved to be a lot more complicated because even if EPiServer Find Client has a interface, nearly all things are done through extension methods and regular Mocking framework does not support that. Luckily for me I have Premium version of Visual Studio 2013 and with that I can use something called Fakes (see links in the end of the blog post) and that framework supports extension methods but I only got it to work in a MSTest project so I created a new project for this.  The first thing you do with Fakes is to right click the reference you need to fake and choose Add Fake Assembly and that will create a new reference to the fake assembly (read more in the links in the end of the post).  After doing that on the EPiServer.Find reference I wrote this test class:

using System.Collections.Generic; using System.Linq; using EPiServer.Find; using EPiServer.Find.Api; using EPiServer.Find.Fakes; using EPiServer.Find.UnifiedSearch; using EPiServerFindTestSite.Business; using FakeItEasy; using Microsoft.QualityTools.Testing.Fakes; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace EpiserverFindTestSite.FakesTest { [TestClass] public class EPiServerFindSearchServiceTests { [TestMethod] public void EPiServerFindSearchService_ShouldReturn_SearchHits() { using (ShimsContext.Create()) { var searchResult = new SearchResult<UnifiedSearchHit>() { Hits = new HitCollection<UnifiedSearchHit>() { Total = 1 } }; searchResult.Hits.Hits = new List<SearchHit<UnifiedSearchHit>>() {new SearchHit<UnifiedSearchHit>() { Id = "Id", Index = "Index", Document = new UnifiedSearchHit() { Excerpt = "Excerpt", Title = "Title", Url = "Url" } }}; ShimSearchExtensions.GetResultITypeSearchOfISearchContentHitSpecificationBoolean = (search, specification, arg3) => new UnifiedSearchResults(searchResult); var iClient = A.Fake<IClient>(); var searchService = new EPiServerFindSearchService(iClient); var result = searchService.SearchFind("Alloy", 40).ToList(); Assert.AreEqual(result.Count(), 1); Assert.AreEqual(result.First().Title, "Title"); } } } }

The class/test it pretty small but it do some strange things. First it uses something called ShimsContext and that is needed to be able to use the faked assembly. After that I create the SearchResult I want to get when the service is calling UnifiedSearchFor and after that I tell the test that when someone call the extension method that looks like this: GetResult(this ITypeSearch<ISearchContent> search, HitSpecification hitSpecification = null, bool filterForPublicSearch = true) it should return the object I have created. I am not replacing the UnifiedSearchFor method because the thing I want to mock is the creating of the search result and that is done with this extension method. After that I search for the text Alloy with Max number of results to 40 and check that I get back one item with the title “Title”.

 

Conclusion

The main thing I have learned from this is that writing Unit Test is pretty simple if you extract away the EPiServer parts as much you can but testing with mocking EPiServer Find is not simple. I had to do a lot of decompiling the code with JustDecompile just to know what I should Mock and not Mock.

Here are some good links:

https://msdn.microsoft.com/en-us/library/hh549175.aspx

http://stacktoheap.com/blog/2012/11/11/testing-extension-methods-with-microsoft-fakes/

Jan 22, 2015

Comments

Please login to comment.
Latest blogs
Optimizely Configured Commerce and Spire CMS - Figuring out Handlers

I recently entered the world of Optimizely Configured Commerce and Spire CMS. Intriguing, interesting and challenging at the same time, especially...

Ritu Madan | Mar 12, 2025

Another console app for calling the Optimizely CMS REST API

Introducing a Spectre.Console.Cli app for exploring an Optimizely SaaS CMS instance and to source code control definitions.

Johan Kronberg | Mar 11, 2025 |

Extending UrlResolver to Generate Lowercase Links in Optimizely CMS 12

When working with Optimizely CMS 12, URL consistency is crucial for SEO and usability. By default, Optimizely does not enforce lowercase URLs, whic...

Santiago Morla | Mar 7, 2025 |

Optimizing Experiences with Optimizely: Custom Audience Criteria for Mobile Visitors

In today’s mobile-first world, delivering personalized experiences to visitors using mobile devices is crucial for maximizing engagement and...

Nenad Nicevski | Mar 5, 2025 |

Unable to view Optimizely Forms submissions when some values are too long

I discovered a form where the form submissions could not be viewed in the Optimizely UI, only downloaded. Learn how to fix the issue.

Tomas Hensrud Gulla | Mar 4, 2025 |

CMS 12 DXP Migrations - Time Zones

When it comes to migrating a project from CMS 11 and .NET Framework on the DXP to CMS 12 and .NET Core one thing you need to be aware of is the...

Scott Reed | Mar 4, 2025