The Good: Why We Love Kentico 😻
In my opinion, Kentico has always been designed to make its users productive. This is true for both the classic Portal Engine drag-and-drop UI builder and the .NET libraries developers can use to interact with the CMS.
Do you need to get user data from the database? Call the static provider class with UserInfoProvider.GetUsers();
. ✔️
Do you need to know which site the current request is running under? Use the static context accessor SiteContext.CurrentSiteName;
. ✔️
Want to ensure some data that is accessible as type object is actually the int value you originally stored? Wrap it in the static utility method call ValidationHelper.GetInteger(originalData, 0);
. ✔️
The key thing to notice about all these calls is that they are static
methods often on static
classes.
These classes don’t require instantiation, we don’t need to understand their dependency graph, they come to us pre-packaged and ready to use, and are truly global values as long as we have access to the .dll
in which they are deployed.
The implication of this design is that all of the above tools that developers have access to, for building complex web sites on the Kentico CMS, can be discovered and leveraged as simply as possible. 🙌
The Bad: Skeletons in the Closet 💀
There is a dark side to this happy world of static
, global access.
These types are often tightly coupled to their dependencies, some can only be used in a specific context (that of a live request going through the ASP.NET request pipeline), and can be difficult — if not impossible — to unit test. 😱
From here on I’m going to refer to things being ‘testable’ or ‘un-testable’ by which I mean unit testing specifically.
This consequence isn’t exactly Kentico’s fault.
ASP.NET Web Forms was not a technology that encouraged testing or a decoupled architecture. It was built to ensure optimal productivity for Windows Forms desktop developers moving to building web applications.
Kentico is built on Web Forms and therefore is going to mimic its patterns and priorities — ease of use, abstracting away the ‘web’ part of web development, and access to static "ambient context" globals like HttpContext
.
If you’ve never heard of Ambient Context before, you can read up on the design pattern here. One item to note is under the Consequences section where it is stated “The consequences to the system of making use of an ambient context object will be dependent on the problem domain”. Here "problem domain" is equivalent to "real running ASP.NET site processing an active HTTP request".
But now we’re starting a new era of Kentico development where our code is going to be run both in the Web Forms CMS architecture and the newer MVC architecture.
MVC was designed to separate concerns, decouple declarative UI from procedural logic, and allow for patterns like Inversion of Control, Dependency Injection and Composition instead of Inheritance. 🤓
How can we, as developers, bridge the gap between the powerful set of libraries and tools provided by Kentico to integrate with the CMS and the best-practice architectural patterns allowed (and encouraged) by MVC? 🥺
Not all of Kentico’s classes have these issues. For example,
ValidationHelper
is static but also basically a pure function that attempts to convert a value of typeobject
to a specific type. It can safely be used anywhere in your code and will give the same results at runtime that it will at test time.
But Wait, There’s Hope! 🤗
Let’s look at a simple example of how to encapsulate the convenient but un-testable parts of our traditional Kentico applications.
For this example, we are going to look at accessing Kentico’s settings API which allows for configuration, normally stored in <appSettings>
in the web.config
of a site to be stored in Kentico’s database.
How might you normally get this data for the site of the current request? 🤔Maybe the following:
string settingValue = SettingsKeyInfoProvider.GetValue("someKeyString", SiteContext.CurrentSiteName);
While Kentico does provide us a way to test SettingsKeyInfoProvider.GetValue()
using Fake<>
in unit tests, there is no way to unit test SiteContext
.
SiteContext
, along with all the other convenient static ***Context
types, are un-testable. They require a real ASP.NET application with a live request in order to work correctly, and because they are static, we cannot mock them. 😒
How do we build our own API that provides access to this data but doesn’t create a dependency on SiteContext
?
Interfaces are Here to Help
The example code below introduces our first and best tool to make our applications more testable: interfaces.
In C#, interfaces can’t be implemented by static
classes, so that immediately saves us from ending up in the situation we already find ourselves in.
But they also provide a seam into which we can insert existing calls to Kentico’s static
classes and methods when needed, without exposing that implementation. No need to throw the baby out with the bathwater. We don’t want to re-write the CMS; we just want it to be testable. 😉
public interface IKenticoSettingConfigProvider | |
{ | |
string GetString(string key, bool global = false); | |
bool GetBool(string key, bool global = false); | |
int GetInt(string key, bool global = false); | |
double GetDouble(string key, bool global = false); | |
decimal GetDecimal(string key, bool global = false); | |
} |
This interface defines a way to access the settings configured for Kentico, both globally and per-site when running a multi-site instance.
Now we have an interface that we can implement however we choose, and as long as our MVC code takes a dependency on this interface (likely through Dependency Injection), we can ensure our code is testable. 👍
Let’s Implement
Okay, so we’ve basically moved the goal post. Our MVC code will depend on IKenticoSettingConfigProvider
, but we will need an implementation for this interface at some point.
Our first thought might be to create a class that directly uses the example SettingsKeyInfoProvider.GetValue()
call above.
public class KenticoSettingConfigProvider : IKenticoSettingConfigProvider | |
{ | |
public string GetString(string key, bool global = false) => | |
global | |
? SettingsKeyInfoProvider.GetValue(key) | |
: SettingsKeyInfoProvider.GetValue(key, SiteContext.CurrentSiteName); | |
// ... additional methods below | |
} |
This certainly works, but if our KenticoSettingConfigProvider
implementation class starts to have any complex logic, we are going to want to test it as well.
Like I mentioned above, while SettingsKeyInfoProvider.GetValue()
is testable, SiteContext
is not — so our initial implementation of this class is, again, not testable. 🤦🏾♂️
A Quick Aside: What’s the Value? 💲
Even if the amount of logic in our implementation class is low and we don’t feel the need to write tests for it, there’s still value in designing it with testing in mind.
To effectively test code, we need to identify dependencies and ensure those dependencies can be mocked or isolated from the code we want to test. This identification process is valuable in itself. By identifying these dependencies, we come to realize what assumptions our code makes about how and where it can be run. 🌟
What does it mean when your code takes a dependency on MemebershipContext
or HttpContext
?
Are these always available when your code is executing? Will they always have the values you expect? What about in a Scheduled Task or a background thread? 🤔
I remember writing some logging code that accessed HttpContext
to log additional request details. Only later did I find out that HttpContext
doesn’t always exist when the logging happens. This caused my logging code to throw exceptions. I made assumptions about where and how my code was run that did not hold true.
Had HttpContext
not been buried deep in a logging method, but instead was specified as a constructor dependency, I would have had a better chance of understanding my failed use-case ahead of time.
Writing for Testability 👩💻
Let’s look at how we can make our KenticoSettingConfigProvider
class testable.
We know SiteContext
isn’t testable, so let’s stop using it directly and instead create a seam we can hide the implementation details behind.
public interface ISiteContext | |
{ | |
string SiteName { get; } | |
int SiteId { get; } | |
SiteInfo Site { get; } | |
} |
Here we create an ISiteContext
interface that exposes the same values we would need from SiteContext
, but since it’s an interface, we can mock it.
If we want our KenticoSettingConfigProvider
to take a dependency on the mocked version of our ISiteContext
when under test, we need to be able to supply that mocked version.
The best way to do this is through constructor Dependency Injection.
public class KenticoSettingConfigProvider : IKenticoSettingConfigProvider | |
{ | |
private readonly ISiteContext siteContext; | |
public KenticoSettingConfigProvider(ISiteContext siteContext) | |
{ | |
Guard.Against.Null(siteContext, nameof(siteContext)); | |
this.siteContext = siteContext; | |
} | |
public string GetString(string key, bool global = false) => | |
global | |
? SettingsKeyInfoProvider.GetValue(key) | |
: SettingsKeyInfoProvider.GetValue(key, siteContext.SiteName); | |
// additional methods below | |
} |
You can see above how we take a dependency on ISiteContext
via the constructor of KenticoSettingConfigProvider
.
If you are wondering about the
Guard.Against.Null()
call in the constructor above, take a look at Steve Smith’s GuardClauses library. It’s a great way to guard against invalid parameters for constructors or methods by throwing an exception if the requirements of the guard are not met.I love how it’s declarative, simple, and easy to reason about. I use them in all the code I write and I write tests that ensure they are in place.
The advantage we gain here is that we can simulate the site of the current request in our tests. When we test the methods of KenticoSettingConfigProvider
we will mock ISiteContext
with an implementation that, for example, always returns "MySite"
for the SiteName
property.
If we Fake<SettingsKeyInfo, SettingsKeyInfoProvider>
and Fake<SiteInfo, SiteInfoProvider>
to match up with "MySite"
, then our ISiteContext
will supply a SiteName
that will match up with the settings we expect our class to return. 👍
The Real Context 🔩🔧
Okay, so we’ve now abstracted our MVC code from the static types in Kentico’s libraries through the IKenticoSettingConfigProvider
interface, and also abstracted our KenticoSettingConfigProvider
away from the un-testable SiteContext
through the ISiteContext
interface.
Let’s implement ISiteContext
with something that will actually work at runtime.
using static CMS.SiteProvider.SiteContext; | |
public class KenticoSiteContext : ISiteContext | |
{ | |
public string SiteName => CurrentSiteName; | |
public int SiteId => CurrentSiteID; | |
public SiteInfo Site => CurrentSite; | |
} |
Well, this is pretty simple, isn’t it? We forward the properties of our interface to the properties of the SiteContext
.
My recommendation is to use an interface like ISiteContext
throughout your MVC codebases whenever you need access to that Kentico data/context goodness, and leave the real SiteContext
as an implementation detail hidden behind ISiteContext
. 👩🏾🔧
This will help make your code more composable, help you identify the dependencies your code has on external data and resources, and perhaps most importantly, help make your code more testable.
You get to leverage the power of Kentico without being subject to the drawbacks of a classic ASP.NET Web Forms influenced architecture! 👏🏽
What’s Next?
Phew 🚴🏽♀️. That was a bit of work, but we’re in a better place because of it. 🗺️
We’ve learned how to identify and isolate the un-testable pieces of Kentico’s infrastructure, and written our own testable library code.
So where do we go from here? 🧗🏻
In my next post, I’ll introduce some techniques and tools to help you test your Kentico code effectively while also reducing boilerplate. 👋🏾
Top comments (0)