A Quick Review
In Part 1 of this series, I detailed why we should be careful taking dependencies on static globals, like SiteContext
in our Kentico code.
To summarize, these static globals provide us access to the state of our application, but they make assumptions about how and where our application is running.
A live ASP.NET Kentico 12 MVC application is a very different thing from a running unit test project, and SiteContext
will provide values that you as the developer have no control over when it is being accessed within a unit test.
The goal of a unit test is to arrange a scenario by simulating an application state, act on that scenario, and then assert on the results of that action. If we do not have the ability to arrange that initial state, as is the case with SiteContext
, we cannot effectively test the units of code that depend on it.
By taking a dependency on an interface instead of the static global, like ISiteContext
, we can use the real SiteContext
as the implementation of that interface at runtime in our application but use a stub at test time.
If we look at the KenticoSettingConfigProvider
we created in Part 1, we can see we have all the pieces in place to write our tests.
Now that we’ve quickly reviewed our previous work, let’s write some tests! 🤓
Writing Our First Test
We create our test class using Kentico’s UnitTests
base class, which is required to be able to interact with *Info
and *InfoProvider
classes without needing to have access to a real database.
We can see here that I’ve created a new test class KenticoSettingConfigProviderTests
.
I’ve told NUnit
that it should discover and run the tests in this class by adding the [TestFixture]
attribute to my class and the [Test]
attribute to my test method.
In this first test I am asserting that if I do not provide a valid parameter to my constructor, then it will throw an exception.
The
.Should()
assertion syntax comes from the FluentAssertions library, which is my preferred tool for writing readable assertions.
So far this is pretty standard unit testing. Let’s move on to testing our Kentico specific code.
Unit Testing Patterns and Conventions
We want to test the various methods of our KenticoSettingConfigProvider
.
It uses Kentico’s SettingKeyInfoProvider
to access settings from the database and our interface ISiteContext
, which is an abstraction over SiteContext
, when we want a site specific setting instead of a global one.
Here is an example of testing the GetString
method to get a site-specific setting value:
There’s a lot going on here that we haven’t discussed yet 🤯, so let’s unpack it.
First, I’ve added a new attribute, [AutoDomainData]
, to the test method. [AutoDomainData]
uses the AutoFixture library to create randomly generated values, for all primitive types, that I can use to set up the state of my test. Any parameter I specify in my test method signature will be populated with a non-null, generated value.
The implementation of this attribute is pretty simple:
🙋You might be wondering, “If it’s so important to control the state of our test, why do we use a library to generate random values to test against?”
That’s a great question!⚡
Some state in a unit test is very specific and needs to have explicitly defined values to ensure the test operates how we want. But other state in our test flows through the entire test without being modified, or if it is modified it is changed in a predictable way, independent of the exact value.
It’s more important that the state is consistent than having a specific set of values.
In the test case above, I’m not actually concerned with the value of the setting I’m trying to retrieve. I’m only attempting to verify it’s the value that SettingKeyInfoProvider
will return for the given setting key.
I’m also not concerned with the exact value of the setting key — the only requirement is that it is directly associated with the value I’m expecting to be returned!😮
I could change this test so that each part of the test state has an explicit value as seen below:
The issue I have with this approach is that now another developer reading the test might wonder why I picked those specific values. Are they important? Do all setting keys need to be suffixed with a numeral? If I use a siteId
other than 3 in a later test, is that significant? 🤔
In order to keep the focus of the test on the “Subject Under Test” (sut
in the examples above), I want to remove any distractions. I consider the values of the state I’m setting up to be distractions in this case. 🧐
While the values of the state might not matter, the state itself has to be internally consistent. If I pass
keyName
to my.GetString()
method but I use a different value when mocking mySettingKeyInfoProvider
data, my test will fail.This is an example of how the state I’m simulating flows through the test, and while the exact state values might not matter, all pieces of my test must use the provided state in the correct way. The state must be consistent.
Faking Data When Testing with Kentico
Let’s now take a look at some of the utilities I’m using to ensure my test state is correctly initialized.
The SettingInfoProviderFixture
is a reusable helper class that can ensure the fake data that SettingInfoProvider
and SiteInfoProvider
access is initialized.
I’m using the Kentico AutomatedTestsWithData
abstract class as a base class for my fixture. This gives me access to the Fake<,>()
, method which is used to initialize the state of data the *InfoProvider
classes will access. 👩💻
Normally you would have these calls to
Fake<,>()
in your test methods, but this setup code is pretty noisy and distracts from the “Subject Under Test”.The
UnitTests
class, from which all Kentico unit test classes must inherit, itself inherits fromAutomatedTestsWithData
so this is a convenient way to move some of that arranging of state out of the test method.
Stubbing Our Context
If we look back at the way we previously defined ISiteContext
, we can see it provides read-only access to a couple of values — Site
, SiteId
, and SiteName
.
The ISiteContext
that is defined as a method parameter in the unit test method is created by AutoFixture with the help of a mocking library NSubstitute.
This ISiteContext
is not null but it also doesn’t have any actual behavior — that is up to us to define.
Above, we can see the call siteContext.SiteName.Returns()
, which is using the NSubstitute .Returns()
extension method. It allows us to explicitly define what value will be returned by this stub during the execution of our test.
We know that KenticoSettingConfigProvider
, internally, uses siteContext.SiteName
to query the SettingInfoProvider
data for a setting value that matches the given setting key and site:
Here again you can see how the randomly generated state flows through our test in a consistent way to ensure that the state we act against is arranged in the same was as a real Kentico site. 👍🏽
Once we have all of our state arranged for our test, we can act on that state by calling .GetString()
on our KenticoSettingConfigProvider
.
Finally, we get a response from the method call, and we can assert that the value returned should match the value we arranged in our initial state configuration.
Executing our unit test shows that the test passes!🌟💥🤸🏾♀️
What We Accomplished
Looking back we can see how several decisions we made in designing our test came together to help us ensure our test would clearly show what was being tested, arrange our state consistently, and also pass despite not running in a live environment.
- Integrating AutoFixture with NUnit allowed us to push the focus of our test away from the exact state we were simulating and onto the behavior of the “Subject Under Test”.✔️
- Moving the setup of Kentico
*InfoProvider
data into a separate fixture class gave us a reusable tool for writing tests and also reduced the state arrangement distractions in our test method.✔️ - Since we relied on
ISiteContext
instead of the static globalSiteContext
we were able to ensure that our arranged state was not only flowed through our method call and*InfoProvider
data stores, but also into the simulated request context that would, in a real running application, reflect the correct Kentico site the current request was associated with.✔️
I hope you found this review, of what I believe to be effective unit testing patterns when working with Kentico, helpful. 👍
Next in this blog series, I’m going to cover some organizational patterns in Kentico 12 MVC projects that can help you scale your applications and manage complexity using thoughtful conventions. 👨🏫
Have you read my previous post in this series “Kentico 12: Design patterns Part 1 — Writing Testable Code”?
Top comments (0)