November Happy Hour will be moved to Thursday December 5th.

Henrik Fransas
Jan 22, 2015
  8439
(1 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

Arve Systad
Arve Systad Jan 23, 2015 01:35 PM

Quick comment: I wouldn't try to test the EPiServerFindSearchService itself, if it's just a wrapper for Find. Basically just because *at some point* you can't test everything, and this might just be that point. Treat it as a data access layer. Imagine Find is a not-mockable service that just exists magically in the background.Then mock your search service class to test controllers or other service classes.

When you attempt to mock all the stuff that Find can return internally, you're really gonna end up testing EPiServer Find as a product - which is not part of your own application. That's EPiServer's job to test, not yours. Rely on it working. If it doesn't, report bugs - don't try to unit test it yourself.

And also, I would call it something simpler like ISearchService/SearchService, since that's what it really does. Whatever engine lies beneath that is completely irrelevant for any consuming classes.

Henrik Fransas
Henrik Fransas Jan 23, 2015 01:57 PM

Thanks Arve

True point on the testing of the search service, I did that most to learn if there were a simple way to mock EPiServer Find and when there were no way I just was not able to stop until I figured out a way to do it anyway :-). What's to be learned from this is that you should have a own implementation/wrapper around your queries to EPiServer Find and have the logic in a layer on top of that so you will be able to test your logic without having to mock EPiServer Find.
In this case there are no logic and the tests does not do much but for a more real implementation I should add another layer between my search service and the controllers where I should put all buisness logic.

You are correct of the naming of the service, I have both the old EPiServer Search service in the project and this so I had to have a different name on it. But a good point, thanks!

Shoma Gujjar
Shoma Gujjar Jun 29, 2015 06:54 PM

Hi,

thank you. Its a very good and informative post. However, i am using Professional VS 2012, i followed the steps you did but for some reason, the SearchClient always returns 0 results. Can you please me and me let me know if i am missing anythiing?

Any help is greatly appreciated.

Please login to comment.
Latest blogs
Optimizely SaaS CMS + Coveo Search Page

Short on time but need a listing feature with filters, pagination, and sorting? Create a fully functional Coveo-powered search page driven by data...

Damian Smutek | Nov 21, 2024 | Syndicated blog

Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog

I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog

Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog