Most of the time, when writing unit tests for your Kentico 12 MVC applications, you can rely on the documented Kentico testing APIs, like the UnitTests
base class and Fake<>()
method.
These APIs allow for Kentico database-dependent types and data sources to be stubbed, without relying on a live connection to the database ๐ช.
But some testing scenarios have solutions that require some insight into how Kentico's internals work ๐ฎ.
Below we'll look at an example of how to test code that uses a custom Page Type that has a database field of type "File".
To learn more about Page Types, check out Kentico's documentation or this post from Chris Hamm over at BizStream ๐.
Defining a Scenario
Custom Page Type - EmployeeContent
Let's set up a scenario which we'd like to test.
Assume we have an EmployeeContent
custom Page Type with the following fields:
-
EmployeeContentID
-int
-
EmployeeContentImage
-Guid
-
EmployeeContentName
-string
-
EmployeeContentHireDate
-DateTime
Here is some of the code that Kentico auto-generates for this Page Type:
public partial class EmployeeContent : TreeNode
{
public const string CLASS_NAME = "Sandbox.EmployeeContent";
private readonly EmployeeContentFields mFields;
[DatabaseIDField]
public int EmployeeContentID
{
get => ValidationHelper
.GetInteger(GetValue("EmployeeContentID"), 0);
set => SetValue("EmployeeContentID", value);
}
[DatabaseField]
public Guid EmployeeContentImage
{
get => ValidationHelper
.GetGuid(GetValue("EmployeeContentImage"), Guid.Empty);
set => SetValue("EmployeeContentImage", value);
}
[DatabaseField]
public string EmployeeContentName
{
get => ValidationHelper
.GetString(GetValue("EmployeeContentName"), @"");
set => SetValue("EmployeeContentName", value);
}
[DatabaseField]
public DateTime EmployeeContentHireDate
{
get => ValidationHelper
.GetDateTime(GetValue("EmployeeContentHireDate"), DateTimeHelper.ZERO_TIME);
set => SetValue("EmployeeContentHireDate", value);
}
}
After the initial class and property definition above, we can find the EmployeeContentFields
nested-class, which gives us a more convenient way of accessing the fields not inherited from TreeNode
(these would be the custom fields defined above).
[RegisterAllProperties]
public partial class EmployeeContentFields :
AbstractHierarchicalObject<EmployeeContentFields>
{
private readonly EmployeeContent mInstance;
public EmployeeContentFields(EmployeeContent instance) =>
mInstance = instance;
public int ID
{
get => mInstance.EmployeeContentID;
set => mInstance.EmployeeContentID = value;
}
public DocumentAttachment Image =>
mInstance.GetFieldDocumentAttachment("EmployeeContentImage");
public string Name
{
get => mInstance.EmployeeContentName;
set => mInstance.EmployeeContentName = value;
}
public DateTime HireDate
{
get => mInstance.EmployeeContentHireDate;
set => mInstance.EmployeeContentHireDate = value;
}
}
In addition to giving us quick access to the fields specific to our custom Page Type, EmployeeContentFields
let's us treat certain types of fields as the data types we expected them to be ๐.
The EmployeeContent.EmployeeContentImage
property and EmployeeContent.EmployeeContentFields.Image
property are tell-tale signs of a custom Page Type with a File
data-type field.
You can read more about managing Page Type fields in Kentico's documentation.
This is a uniquely named page attachment, as opposed to the TreeNode.Attachments
property, that EmployeeContent
inherits.
TreeNode.Attachments
is a collection of attachments for the page which don't have named associations to the page itself. They aren't properties exposed on Page Type class - just a bag of Attachments ๐ค.
This is different from fields on the Page Type that have a data-type specified as "File". Since these are specific fields they will have specific properties on the Page Type class that allow you to access the Attachments ๐ง.
Notice that while EmployeeContentImage
is type Guid
(which is the Guid
of the image attachment associated with this field), in the EmployeeContentFields
class we have an Image
property of type DocumentAttachment
.
[DatabaseField]
public Guid EmployeeContentImage
{
get => ValidationHelper
.GetGuid(GetValue("EmployeeContentImage"), Guid.Empty);
set => SetValue("EmployeeContentImage", value);
}
vs
public DocumentAttachment Image =>
mInstance.GetFieldDocumentAttachment("EmployeeContentImage");
While the Guid
is the true raw value stored in the database column, the DocumentAttachment
is probably what we want to interact with when using this custom Page Type class.
DocumentAttachment
gives us access to the.GetPath()
method which uses Kentico's internals to build a URL to the attachment ๐, as opposed to doing the string building of URL ourselves.
This Image
property does help us out, but it also makes our class tricky to test. This property is read-only! We can't assign a test value to an instance of EmployeeContent
that we might create for testing ๐.
Also, what does .GetFieldDocumentAttachment()
do? We can't intercept this call so we'll need to rely on Kentico's testing APIs to help ๐.
Request Handler - EmployeesListPageRequestHandler
Following the Mediator pattern we move our business logic and request processing out of our MVC Controller and into a Request Handler.
We'll be using the Mediatr library to help us build out this infrastructure.
This Request Handler class, which we will name EmployeesListPageRequestHandler
, will get a request from a Controller, process it, and return a View Model as a response.
We could retrieve the data from the auto-generated EmployeeContentProvider
but that's going to likely force us to write an integration test for this Request Handler.
Instead we will hide the data-access implementation details behind a facade.
It could be a Repository, a Query, or a Dispatcher which maps a Query to a Query Handler (similar to how Mediatr works).
The point is, you'll want some sort of abstraction to keep your Request Handler loosely coupled to your data access technology.
A Quick Note on Repositories ...
I'm not a fan of custom written Repositories for several reasons.
We often work with Object Relational Mappers (ex: Entity Framework or Kentico's
ObjectQuery<T>
,DocumentQuery<T>
, and static*Provider
types) which are Repositories already, so wrapping a Repository in another one feels like an abuse of the pattern.Custom written Repositories become large bags of methods all dealing with the same type (ex:
EmployeeContent
) but for different purposes. They break the Single Repsonsibility Principle and typically don't have a reason to contain all the methods they contain since the Repository classes themselves don't have any shared state between methods.
This is why I instead favor using the Command / Query Resposibility Segregation pattern and using a Query, QueryHandler, and a Dispatcher ๐ช.
I'm going to use a simple Query
below, but use the pattern you're most familiar with ๐ค.
And, of course, we'll be using Constructor-based Dependency Injection for testability ๐!
Here is the simple implementation of our Request Handler, which is the code we will be writing a unit test for.
public class EmployeesListPageRequestHandler
: RequestHandler<EmployeeListPageRequest, EmployeeListPageViewModel>
{
private readonly IEmployeeQuery query;
public EmployeesListPageRequestHandler(IEmployeesQuery query)
{
this.query = query;
}
protected override EmployeeListPageViewModel Handle(
EmployeeListPageRequest request)
{
IEnumerable<EmployeeContent> employees = query.Execute(request.EmployeeCount);
if (employees is null)
{
return new EmployeeListPageViewModel(
employees: Enumerable.Empty<EmployeeContent>(),
count: 0);
}
var employeeViewModels = employees.Select(e =>
{
string imageUrl = e.Fields.Image.GetPath();
new EmployeeViewModel(
name: e.Fields.Name,
imageUrl: imageUrl,
hireDate: e.Fields.HireDate
}));
return new EmployeeListPageViewModel(
employees: employeeViewModels,
count: employeeViewModels.Count());
}
}
This class retrieves EmployeeContent
, presumably, from the database, handles invalid data, and returns a valid View Model instance.
Pretty normal use-case!
Setting Up Our Test
Now that we've scaffoled out all the code we want to test and identified they key areas our test will interact with, let's get to creating our unit test ๐ฅณ!
Creating the Test Class - EmployeesListPageRequestHandlerTests
First things first - let's create a test class and test method following Kentico's documentation for unit tests.
using CMS.Tests;
using NUnit.Framework;
using using System.Threading.Tasks;
[TestFixture]
public class EmployeesListPageRequestHandlerTests : UnitTests
{
[Test]
public async Task Handle_Will_Initialize_ViewModel()
{
// Arrange
// Act
// Assert
}
}
We need to inherit from UnitTests
in the CMS.Tests
namespace.
This will let us tell Kentico we want this test to work with data that would normally come from the database (our custom Page Type instances), but we will providing the data ourselves within the test ๐.
We will also separate our test method into 3 phases - Arrange, Act, and Assert.
Arranging Our Test - AutoFixture, NSubstitute, and Kentico Fakes
Now let's begin the "Arrange" phase of the test.
We will be using AutoFixture to create values for our test.
Most of the time the actual values used in a test are not important.
The values are passed around and maybe modified, but as long as the values at the end of the test are consistent with the values at the beginning - given the process of the Subject Under Test (sut
) - we don't really care what the original values were.
AutoFixture allows us to create valid values on-the-fly.
Here is an example below:
using AutoFixture;
...
// Arrange
var fixture = new DomainFixture();
string name = fixture.Create<string>();
// Do something with name ...
Now we need to tell Kentico that we want to create fake instances of our EmployeeContent
Page Type and we will supply them as values returned from our EmployeesQuery
.
First, let's create the fake EmployeeContent
instances.
We need the following using
statements for namespace imports from Kentico:
using CMS.DataEngine;
using CMS.DocumentEngine;
using CMS.DocumentEngine.Types.Sandbox;
using CMS.Tests;
using Tests.DocumentEngine;
We will use the Fake()
method we have access to by inheriting from UnitTests
to tell Kentico we want to fake access to some data.
We will also use the DocumentType<T>()
extension method to tell Kentico what type of Page Type we will be faking.
Fake().DocumentType<EmployeeContent>(EmployeeContent.CLASS_NAME);
The above line only tells Kentico our intentions - it doesn't actually create any instances of EmployeeContent
, let's do that now:
var employee = TreeNode.New<EmployeeContent>().With(e =>
{
e.Fields.Name = fixture.Create<string>();
e.Fields.HireDate = fixture.Create<DateTime>();
e.EmployeeContentImage = fixture.Create<Guid>();
});
You'll notice that we don't assign a value to e.Fields.Image
because it's read-only, so instead we assign a Guid
to its backing field, EmployeeContentImage
.
But how do we turn that Guid
into an instance of DocumentImage
when we call EmployeeContent.Fields.Image
๐ค?
We will need Kentico to do that for us, but that will require mocking a little more data:
var attachmentGuid = fixture.Create<Guid>();
Fake<AttachmentInfo, AttachmentInfoProvider>()
.WithData(new AttachmentInfo
{
AttachmentGUID = attachmentGuid
});
Fake().DocumentType<EmployeeContent>(EmployeeContent.CLASS_NAME);
var employee = TreeNode.New<EmployeeContent>().With(e =>
{
e.Fields.Name = fixture.Create<string>();
e.Fields.HireDate = fixture.Create<DateTime>();
e.EmployeeContentImage = attachmentGuid;
});
Notice how we use the attachmentGuid
value both for the faked AttachmentInfo.AttachmentGUID
property and the EmployeeContent.EmployeeContentImage
property.
This will allow Kentico to tie these two instances together at test-runtime.
As it turns out TreeNode.GetFieldDocumentAttachment()
calls AttachmentInfoProvider.GetAttachmentInfo()
, passing the Guid
of the attachment to be retrieved.
So, as long as we fake an AttachmentInfo
using AttachmentInfoProvider
, we will be able to setup the state of our test to work entirely in memory and not need a database ๐ค๐๐ฅ.
Now we need a way to supply the employee
we created above as the value our IEmployeeQuery
returns for the Execute()
call.
We can use NSubstitute to stub our interfaces with minimal implementations that only return values without performing any operations.
using NSubstitute;
// Arrange
var query = Substitute.For<IEmployeeQuery>();
query
.Execute(Arg.Is(request.EmployeeCount))
.Returns(new [] { employee });
Let's put all of the pieces of the Arrange phase of our test together:
[Test, AutoDomainData]
public async Task public async Task Handle_Will_Initialize_ViewModel()
{
// Arrange
var attachmentGuid = fixture.Create<Guid>();
Fake<AttachmentInfo, AttachmentInfoProvider>()
.WithData(new AttachmentInfo
{
AttachmentGUID = attachmentGuid
});
Fake().DocumentType<EmployeeContent>(EmployeeContent.CLASS_NAME);
var employee = TreeNode.New<EmployeeContent>().With(e =>
{
e.Fields.Name = fixture.Create<string>();
e.Fields.HireDate = fixture.Create<DateTime>();
e.EmployeeContentImage = attachmentGuid;
});
var request = fixture.Create<EmployeeListPageRequest>();
var query = Substitute.For<IEmployeeQuery>();
query
.Execute(Arg.Is(request.EmployeeCount))
.Returns(new [] { employee });
IRequestHandler<EmployeeListPageRequest, EmployeeListPageViewModel> sut =
new NewsRequestHandler(query);
// Act
// Assert
}
Acting Out Our Test Case
Now for the simple part - the Act phase:
var viewModel = await sut.Handle(request, default);
Yup, that's it. All the work typically goes into the Arrange and Assert phases of our test since the Act phase has already been written - it's the body of the method you are testing!
Asserting On Our Test Results - FluentAssertions
We now need to Assert that the state of our test data is exactly what we expect it to be after the Act phase.
We can do this using .NET's built-in Assert calls (which are in the Microsoft.VisualStudio.TestTools.UnitTesting
namespace), but NUnit
already supplies us some in the NUnit.Framework
namespace.
That said, my preference is to use a library like FluentAssertions to provide a more Behavior Driven Development style of assertions which, I find, makes the test more readable ๐.
FluentAssertions uses extension methods to provide a syntax that looks as follows:
SomeViewModel viewModel = ...
viewModel.Should().NotBeNull();
viewModel.FirstName.Should().Be("abc");
So let's use FluentAssertions to Assert against the state of our test after we've acted upon our Subject Under Test:
I'll add in pieces from the Arrange and Act phases for reference.
// Arrange
...
var employee = TreeNode.New<EmployeeContent>().With(e =>
{
e.Fields.Name = fixture.Create<string>();
e.Fields.HireDate = fixture.Create<DateTime>();
e.EmployeeContentImage = attachmentGuid;
});
...
// Act
var viewModel = await sut.Handle(request, default);
// Assert
viewModel.Count.Should().Be(1);
var employeeViewModel = viewModel.Employees.First();
employeeViewModel.Name.Should().Be(employee.Fields.Name);
employeeViewModel.HireDate.Should().Be(employee.Fields.HireDate);
employeeViewModel.ImageUrl
.Should()
.Contain(employee.EmployeeContentImage.ToString());
Our assertions here are stating the following things:
- There should only be (1) employee in the View Model.
- The values of the
EmployeeViewModel
should match up with the values of theEmployeeContent
instance. - The
EmployeeViewModel.ImageUrl
should contain theGuid
as a string matching theGuid
ofEmployeeViewModel.EmployeeContentImage
.
For the last assertion we could be more specific and ensure the URL created by Kentico's DocumentAttachment.GetPath()
method matches what we expect it to be, character by character, but I'm ok with what we have.
Summary
Look at the beautiful mess we've made ๐คฃ!
We looked at some design patterns that will help us architect our Kentico MVC application, and set up our business logic (mostly data access at this point), to be 100% testable ๐!
We also used Kentico's testing APIs to help us mock data and ensure our test could be run as a Unit Test and not an Integration Test ๐.
This will make the test faster to run and less brittle as our application changes ๐.
Finally we leveraged some .NET testing libraries (AutoFixture, NSubstitute, and FluentAssertions) to help us focus on the key phases of our test - Arrange, Act, and Assert ๐.
We left out a key test scenario - what if the data returned by our
IEmployeesQuery
isnull
? We handle this in our Request Handler, but we don't test for this scenario.This test case should be a separate test method where we Arrange, Act, and Assert all over again, but this time with the Arrange and Assert phases set up for a
null
data scenario.I'll leave this up to you to implement ๐ค!
If you have any feedback, or recommended ways you like to write your tests, I'd love to hear about it in the comments below.
Thanks for reading!
If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:
Or my Kentico blog series:
Top comments (0)