Try our conversational search powered by Generative AI!

Stefan Forsberg
Nov 26, 2010
  6779
(0 votes)

Making episerver code testable– the fallback language function

So, after the last post that tried to introduce some concepts and tools this posts is going to be more practical and hands-on.

If there’s one thing I want you to keep in mind is the following: When talking about “testing with EPiServer” what you really want to do is to be able to test your interaction with EPiServer, not EPiServer itself.

The fallback language method:

Let’s take a look at the code we want to test

   1: /// <summary>
   2: /// Get translation using CMS setting for fallback language
   3: /// </summary>
   4: /// <param name="key">Lang-file path</param>
   5: public static string TranslateWithFallbackLanguageBranch(this LanguageManager mgr, string key)
   6: {
   7:     string fallbackLanguageID = GetFallbackLanguageIDFromCMS();
   8:  
   9:     return mgr.TranslateFallback(key, mgr.Translate(key, fallbackLanguageID));
  10: }
   1: /// <summary>
   2: /// Get the fallback lanuage from the CMS
   3: /// </summary>
   4: private static string GetFallbackLanguageIDFromCMS()
   5: {
   6:     // Get PageBase using the current handler
   7:     var page = HttpContext.Current.Handler as PageBase;
   8:  
   9:     // Get the page language setting from the root page, using the current page's language
  10:     // This is where the replacement and fallback languages are defined
  11:     PageLanguageSetting pageLanguageSetting = PageLanguageSetting.Load(PageReference.RootPage, page.CurrentPage.LanguageID);
  12:     
  13:     if (pageLanguageSetting != null)
  14:     {
  15:         // Check if there actually is a fallback language defined
  16:         // If so, use the first one
  17:         if (pageLanguageSetting.LanguageBranchFallback.Length > 0)
  18:             return pageLanguageSetting.LanguageBranchFallback[0];
  19:     }
  20:  
  21:     // If there is no fallback language defined then we return the current page's language ID
  22:     return page.CurrentPage.LanguageID;
  23: }

Let’s do a quick analysis of what’s going on here.

A extension method for the LanguageManager. Extension method means static.

Getting a page from the handler in HttpContext

Loading the PageLanguageSettings from the root page

If a fallback language exists return it, otherwise use the language defined in the current page.

SRP and testing

Consider the method GetFallbackLanguageIDFromCMS. The first thing that happens is that it tries to cast the Handler of the current http context to a page. If we for a moment ignore the fact that the HttpContext.Current is null outside a web context we still have problems. Why’s that? Imagine we want to test things that happens after that casting, like the scenario that the language from the page should be used in case a fallback language can’t be fetched. We would have to make sure that the casting succeeded even if that’s not what we want to test at the moment.

A lot of the design principles didn’t make sense until I started to write tests. And IoC-containers didn’t make much sense until I had code that followed the SOLID principles. It sure is possible to write tests while ignoring SOLID and writing a system that uses DI/IoC without a IoC-container. It’s also possible to follow google maps directions and Jet Ski your way across the Pacific ocean, but in practice it might not be the best way. 

So, let’s brake up this method into a few specialized classes.

Parsing PageData from HttpContext

What we’re interested in here is getting a PageData object from the HttpContext so let’s create an abstraction for that

   1: public interface IPageDataFromHttpHandlerParser
   2: {
   3:     PageData GetPage();
   4: }
   1: public class PageDataFromHttpHandlerParser : IPageDataFromHttpHandlerParser
   2: {
   3:     private readonly HttpContextBase _httpContext;
   4:  
   5:     public PageDataFromHttpHandlerParser(HttpContextBase httpContext)
   6:     {
   7:         _httpContext = httpContext;
   8:     }
   9:  
  10:     public PageData GetPage()
  11:     {
  12:         var page = _httpContext.Handler as PageBase;
  13:         return page.CurrentPage;
  14:     }
  15: }

 

Loading PageLanguageSettings

This code requires an EPiServer context. with all the configuration that comes with it. You might be thinking “So? You can easily do that outside of a web context. Haven’t you seen that you can run EPiServer as a console app?” Yes, I have. To understand why I don’t want to do that we have to quickly discuss the difference between unit testing and integration testing. In these unit tests I’m writing in this post I’m only concerned about the class I’m testing. I don’t want to know or care about anything happening outside it. If I did want to test the whole suite that would be integration testing. Why don’t I want to do that? Well, in essence I would be testing EPiServer and I’d rather leave that to EPiServer themselves. It’s the same line of thought as the previous post where I wanted to make sure that DateFactory.Save is being called but I don’t care about what that method does. I’m trusting that method to do it’s job.

So, once again (detecting a pattern here?) let’s create an abstraction for loading a PageLanguageSetting. This is a abstract class / wrapper class pattern that is used for HttpContextBase/HttpContextWrapper and most (if not all?) classes in EPiAbstractions.

   1: public interface IPageLanguageSettingFacade
   2: {
   3:     PageLanguageSetting Load(PageReference pageReference, string language);
   4: }

Getting a fallback language

The method GetFallbackLanguageIDFromCMS basically only get’s a language ID to use as a fallback. Using our newly created abstractions as well as an abstractions for a PageReference (from EPiAbstractions) we can rewrite this method to work against those abstractions instead of concrete classes.

Our class GetFallbackLanguageFromRootBasedOnCurrentPage depends on our abstraction and takes instances of them in it’s constructor

   1: private readonly IPageLanguageSettingFacade _pageLanguageSettingFacade;
   2: private readonly IPageReferenceFacade _pageReferenceFacade;
   3: private readonly IPageDataFromHttpHandlerParser _pageDataFromHttpHandlerParser;
   4:  
   5: public GetFallbackLanguageFromRootBasedOnCurrentPage(IPageLanguageSettingFacade pageLanguageSettingFacade, IPageReferenceFacade pageReferenceFacade, IPageDataFromHttpHandlerParser pageDataFromHttpHandlerParser)
   6: {
   7:     _pageLanguageSettingFacade = pageLanguageSettingFacade;
   8:     _pageReferenceFacade = pageReferenceFacade;
   9:     _pageDataFromHttpHandlerParser = pageDataFromHttpHandlerParser;
  10: }

The method itself looks pretty much the same as it did before

   1: public string GetFallbackLanguage()
   2: {
   3:     // Get PageBase using the current handler
   4:     var page = _pageDataFromHttpHandlerParser.GetPage();
   5:  
   6:     // Get the page language setting from the root page, using the current page's language
   7:     // This is where the replacement and fallback languages are defined
   8:     PageLanguageSetting pageLanguageSetting = _pageLanguageSettingFacade.Load(_pageReferenceFacade.RootPage, page.LanguageID);
   9:  
  10:     if (pageLanguageSetting != null)
  11:     {
  12:         // Check if there actually is a fallback language defined
  13:         // If so, use the first one
  14:         if (pageLanguageSetting.LanguageBranchFallback.Length > 0)
  15:             return pageLanguageSetting.LanguageBranchFallback[0];
  16:     }
  17:  
  18:     // If there is no fallback language defined then we return the current page's language ID
  19:     return page.LanguageID;
  20: }

 

Show me some tests already

Ok, let’s see how the above refactoring enable us to test the GetFallbackLanguageFromRootBasedOnCurrentPage class. First I’ve created a SetUp method that does initialization of the mocks and classes used in the tests

   1: [SetUp]
   2: public void Setup()
   3: {
   4:     _pageLanguageSettingFacadeMock = new Mock<IPageLanguageSettingFacade>();
   5:     _pageReferenceFacadeMock = new Mock<IPageReferenceFacade>();
   6:     _pageDataFromHttpHandlerParserMock = new Mock<IPageDataFromHttpHandlerParser>();
   7:  
   8:     _getFallbackLanguageFromRootBasedOnCurrentPage = new GetFallbackLanguageFromRootBasedOnCurrentPage(
   9:         _pageLanguageSettingFacadeMock.Object,
  10:         _pageReferenceFacadeMock.Object,
  11:         _pageDataFromHttpHandlerParserMock.Object
  12:         );
  13: }

Test 1 - No language setting defined

Let’s begin with this test: Given that no language setting can be fetched from the root page when getting fallback language based on a page then the language from the page is used

So to setup this test we first need a PageData that’s to be returned from the parsing of a PageData from the Handler.

   1: var page = new PageData();
   2: page.Property.Add("PageLanguageBranch", new PropertyString("en"));
   3:  
   4: _pageDataFromHttpHandlerParserMock
   5:     .Setup(x => x.GetPage())
   6:     .Returns(page);

Notice here that while the concrete implementation of PageDataFromHttpHandler has a dependency to HttpContextBase because we’re working against an abstraction we can ignore that here and just focus on the interaction with the class.

Now need to make sure that when attempting to load the PageLanguageSettings nothing is returned (eg, the PageLanguageSetting is null).

   1: _pageLanguageSettingFacadeMock
   2:     .Setup(x => x.Load(It.IsAny<PageReference>(), "en"))
   3:     .Returns((PageLanguageSetting)null);

The Moq syntax for It.IsAny always seems quite confusing at first. When you do your setup you can instruct Moq to return different results depending on what the in parameters are. So we could specify that one PageLaguageSetting should be returned if the PageReference was StartPage and another if the PageReference was the Basket. What the It.IsAny does is basically saying “regardless of which PageReference is sent in, return this”.

The setup is now complete and we can call the method that does the actually fetching of the fallback language.

   1: var result = _getFallbackLanguageFromRootBasedOnCurrentPage.GetFallbackLanguage();

And then assert that the language defined on the page is indeed returned

   1: Assert.That(result, Is.EqualTo("en"));

Test 2 – no fallback language defined

In this scenario we’re going to write the following test: Given that no fallback language is defined when getting fallback language based on a page then the language from the page is used

We need the same parsing of a page as in the last test

   1: var page = new PageData();
   2: page.Property.Add("PageLanguageBranch", new PropertyString("en"));
   3:  
   4: _pageDataFromHttpHandlerParserMock
   5:     .Setup(x => x.GetPage())
   6:     .Returns(page);

Now, instead of returning null we need to return a PageLanguageSetting that contains an empty array of fallback languages

   1: var pageLanguageSetting = new PageLanguageSetting
   2: {
   3:     LanguageBranchFallback = new string[] { }
   4: };
   5:  
   6: _pageLanguageSettingFacadeMock
   7:     .Setup(x => x.Load(It.IsAny<PageReference>(), "en"))
   8:     .Returns(pageLanguageSetting);

The act and assert is the same as the previous test

   1: // Act
   2: var result = _getFallbackLanguageFromRootBasedOnCurrentPage.GetFallbackLanguage();
   3:  
   4: // Assert
   5: Assert.That(result, Is.EqualTo("en"));

Test 3 – fallback language is defined

So let’s test the scenario when a fallback language is defined: Given that a fallback language is defined when getting fallback language based on a page then fallback language is returned

So, the difference from the previous test is that we need to make sure that a fallback language is defined

   1: var pageLanguageSetting = new PageLanguageSetting
   2: {
   3:     LanguageBranchFallback = new string[] { "sv" }
   4: };
   5:  
   6: _pageLanguageSettingFacadeMock
   7:     .Setup(x => x.Load(It.IsAny<PageReference>(), "en"))
   8:     .Returns(pageLanguageSetting);

We also need to assert that the resulting language id is the one defined as the fallback language

   1: Assert.That(result, Is.EqualTo("sv"));

 

So, by abstracting away things we don’t care about we’re able to write tests that assumes that other classes do their job so that we can focus on (testing) the class at hand.

image

 

Testing LanguageManager

Well, I’ll be honest. Testing LanguageManager doesn’t make a whole lot of sense. Why is that? Let’s look the the method signature. The first parameter is the key and the second is the string to return if no item was found for the current language. We can’t really test that method since that would be testing EPiServer functionality (eg, how do we determine if a key is found or not). I hope this doesn’t make you feel cheated.

Testing and extension methods

Extension methods are static. Static members and testing are not the best of friend. Why is that? Well, for one thing you can’t use constructor injection. While it is possible to use some variation of setter injection all in all I tend to avoid using static members because most of the time they aren’t really needed.

Many people do however like the extension methods so how do we go on about making them testable? Well I haven’t really found any way that feels optimal but what I’ve settled for in the meantime is to have some façade and let the extension method work against that façade.

The extension method originally looked like this:

   1: public static string TranslateWithFallbackLanguageBranch(this LanguageManager languageManager, string key)
   2: {
   3:     string fallbackLanguageID = GetFallbackLanguageIDFromCMS();
   4:  
   5:     return languageManager.TranslateFallback(key, languageManager.Translate(key, fallbackLanguageID));
   6: }

What I’ve done is create a non static class that handles the logic

   1: public class TranslateWithFallbackLanguageBranch
   2: {
   3:     private readonly LanguageManager _languageManager;
   4:     private readonly IGetFallbackLanguage _getFallbackLanguage;
   5:  
   6:     public TranslateWithFallbackLanguageBranch(LanguageManager languageManager, IGetFallbackLanguage getFallbackLanguage)
   7:     {
   8:         _languageManager = languageManager;
   9:         _getFallbackLanguage = getFallbackLanguage;
  10:     }
  11:  
  12:     public string Translate(string key)
  13:     {
  14:         string fallbackLanguageId = _getFallbackLanguage.GetFallbackLanguage();
  15:  
  16:         return _languageManager.TranslateFallback(key, _languageManager.Translate(key, fallbackLanguageId));
  17:     }
  18: }

The extension method then just news up this class and uses it

   1: public static string TranslateWithFallbackLanguageBranch(this LanguageManager languageManager, string key)
   2: {
   3:     return new TranslateWithFallbackLanguageBranch(languageManager,
   4:         new GetFallbackLanguageFromRootBasedOnCurrentPage(
   5:             new PageLanguageSettingFacade(), 
   6:             new PageReferenceFacade(), 
   7:             new PageDataFromHttpHandlerParser(
   8:                 new HttpContextWrapper(HttpContext.Current)
   9:                 )
  10:             )
  11:         )
  12:         .Translate(key);
  13: }
Does the newing up of  seem like a hassle? It does, doesn’t it? This ties in to the point I tried to make before, that while it’s possible to write an application without an IoC-container it’s not a very good idea in the real world. But this post doesn’t really deal with that, if you’re interested in solving it take a look at my introduction to StructureMap.

That settles this post. I hope that this has shed some light on how you, today, can test your interaction with EPiServer. The next post will contain more examples with refactoring of EPiCode.Extensions as well as a look at MSpec.

Nov 26, 2010

Comments

Please login to comment.
Latest blogs
Solving the mystery of high memory usage

Sometimes, my work is easy, the problem could be resolved with one look (when I’m lucky enough to look at where it needs to be looked, just like th...

Quan Mai | Apr 22, 2024 | Syndicated blog

Search & Navigation reporting improvements

From version 16.1.0 there are some updates on the statistics pages: Add pagination to search phrase list Allows choosing a custom date range to get...

Phong | Apr 22, 2024

Optimizely and the never-ending story of the missing globe!

I've worked with Optimizely CMS for 14 years, and there are two things I'm obsessed with: Link validation and the globe that keeps disappearing on...

Tomas Hensrud Gulla | Apr 18, 2024 | Syndicated blog

Visitor Groups Usage Report For Optimizely CMS 12

This add-on offers detailed information on how visitor groups are used and how effective they are within Optimizely CMS. Editors can monitor and...

Adnan Zameer | Apr 18, 2024 | Syndicated blog

Azure AI Language – Abstractive Summarisation in Optimizely CMS

In this article, I show how the abstraction summarisation feature provided by the Azure AI Language platform, can be used within Optimizely CMS to...

Anil Patel | Apr 18, 2024 | Syndicated blog

Fix your Search & Navigation (Find) indexing job, please

Once upon a time, a colleague asked me to look into a customer database with weird spikes in database log usage. (You might start to wonder why I a...

Quan Mai | Apr 17, 2024 | Syndicated blog