DEV Community

Sean G. Wright
Sean G. Wright

Posted on • Updated on

Kentico 12: Design Patterns Part 11 - Unit Testing Custom Page Types

Books and pages

Photo by Patrick Tomasso on Unsplash

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

vs

public DocumentAttachment Image =>
    mInstance.GetFieldDocumentAttachment("EmployeeContentImage");
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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.

  2. 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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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 ...
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>();
});
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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 });

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Acting Out Our Test Case

Now for the simple part - the Act phase:

var viewModel = await sut.Handle(request, default);
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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 the EmployeeContent instance.
  • The EmployeeViewModel.ImageUrl should contain the Guid as a string matching the Guid of EmployeeViewModel.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 is null? 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:

#kentico

Or my Kentico blog series:

Oldest comments (0)