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. š
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.
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.
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.
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.
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)