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.
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: }
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.
Comments