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
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
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.
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
The method itself looks pretty much the same as it did before
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
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.
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).
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.
And then assert that the language defined on the page is indeed returned
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
Now, instead of returning null we need to return a PageLanguageSetting that contains an empty array of fallback languages
The act and assert is the same as the previous test
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
We also need to assert that the resulting language id is the one defined as the fallback language
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.
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:
What I’ve done is create a non static class that handles the logic
The extension method then just news up this class and uses itDoes 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.