Xperience unit test mocking of content query executor

IvanPeev February 29, 2024 12:15 PM

An Xperience project I'm working on contains the following interface:

public interface IContentRepository
{
    Task<IEnumerable<T>> GetWebPageAsync<T>(
       ContentItemQueryBuilder builder,
       Cancellationtoken cancellationToken
    ) where T : new();
}

The implementation of this is:

public class ContentRepository : IContentRepository
{
   private readonly IContentQueryExecutor _contentQueryExecutor;
   private readonly IWebPageQueryResultMapper _webPageQueryResultMapper;
   private readonly IWebsiteChannelContext _websiteChannelContext;

   public ContentRepository(
      IContentQueryExecutor contentQueryExecutor,
      IWebPageQueryResultMapper webPageQueryResultMapper,
      IWebsiteChannelContext websiteChannelContext,
   )
   {
       _contentQueryExecutor = contentQueryExecutor;
       _webPageQueryResultMapper = webPageQueryResultMapper;
       _websiteChannelContext = websiteChannelContext;
   }

   public async Task<IEnumerable<T>> GetWebPageAsync<T>(
      ContentItemQueryBuilder builder,
      CancellationToken cancellationToken
   ) where T : new()
   {
       var queryOptions = new ContentQueryExecutionOptions()
       {
          ForPreview = _websiteChannelContext.IsPreview
       };

       return await _contentQueryExecutor.GetWebPageResult(
          builder: builder,
          resultSelector: _webPageQueryResultMapper.Map<T>,
          options: queryOptions,
          cancellationToken: cancellationToken
       );
    }
}

The idea behind this repository is that you can just do a single call to get web pages, content and such. For testing the method above I need to mock the _contentQueryExecutor.GetWebPageResult call to return an IEnumerable of the given page type. However nothing really seems to work. In my test file for example I do the following:

var homePage = new HomePage
{
   SystemFields = new WebPageFields
   {
       WebPageItemID = 1
   }
};

_webPageQueryResultMapper
   .Map<HomePage>(Arg.Any<IWebPageContentQueryDataContainer>())
   .Returns(homePage);

_contentQueryExecutor
    .GetWebPageResult(
        builder: Arg.Any<ContentItemQueryBuilder>(),
        resultSelector: _webPageQueryResultMapper.Map<HomePage>,
        options: Arg.Any<ContentQueryExecutionOptions>(),
        cancellationToken: Arg.Any<CancellationToken>()
    )
    .Returns([homePage]);

But the result always seems to be null from the _contentQueryExecutor. The _webPageQueryResultMapper returns the correct value however. I'm using NSubstitute and xUnit for my unit tests.

Answers

🔗 Sean Wright (seangwright) March 25, 2024 6:16 PM

@IvanPeev

I can't speak to xUnit compatibility since our unit and integration testing support is designed only for NUnit.

I re-created your example test in Xperience v28.4.1 using NUnit.

dotnet new update

dotnet new kentico-xperience-sample-mvc -n DancingGoat -o xk-28-04-01-01\DancingGoat

cd xk-28-04-01-01

dotnet new nunit -n DancingGoat.Tests -o .\DancingGoat.Tests

dotnet add .\DancingGoat.Tests\ reference .\DancingGoat\

dotnet add .\DancingGoat.Tests\ package NSubstitute

Then I added the test code:

using CMS.ContentEngine;
using CMS.Websites;
using DancingGoat.Models;
using NSubstitute;

namespace DancingGoat.Tests;

public class Tests
{
    [Test]
    public void Test1()
    {
        var _webPageQueryResultMapper = Substitute.For<IWebPageQueryResultMapper>();
        var _contentQueryExecutor = Substitute.For<IContentQueryExecutor>();

        var homePage = new HomePage
        {
            SystemFields = new WebPageFields
            {
                WebPageItemID = 1
            }
        };

        _webPageQueryResultMapper
           .Map<HomePage>(Arg.Any<IWebPageContentQueryDataContainer>())
           .Returns(homePage);

        _contentQueryExecutor
            .GetWebPageResult(
                builder: Arg.Any<ContentItemQueryBuilder>(),
                resultSelector: _webPageQueryResultMapper.Map<HomePage>,
                options: Arg.Any<ContentQueryExecutionOptions>(),
                cancellationToken: Arg.Any<CancellationToken>()
            )
            .Returns([homePage]);
    }
}

Running the tests results in a passing test

dotnet test

Microsoft (R) Test Execution Command Line Tool Version 17.9.0 (x64)
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.

Passed!  - Failed:     0, Passed:     1, Skipped:     0, Total:     1, Duration: 53 ms - DancingGoat.Tests.dll (net8.0)

Maybe there's some incompatibility with Xunit? I know I ran into issues with it in the past when developing for previous versions of Kentico.

Or maybe there was an unidentified bug with the version of Xperience by Kentico you are using that has been patched as of v28.4.1?


🔗 IvanPeev March 27, 2024 10:19 AM

@seangwright

So in the example above in test method the given functions are mocked which is syntactically correct and the test passes! It's exactly what you'd expect. However the real issue starts when you do the actual calls to the mocked functions to verify that they return the correct values.

Let's extend the test method by adding a couple of calls and storing them in variables:

[Fact]
public async void Test1()
{
    var _webPageQueryResultMapper = Substitute.For<IWebPageQueryResultMapper>();
    var _contentQueryExecutor = Substitute.For<IContentQueryExecutor>();

    var homePage = new HomePage
    {
        SystemFields = new WebPageFields
        {
            WebPageItemID = 1
        }
    };

    _webPageQueryResultMapper
       .Map<HomePage>(Arg.Any<IWebPageContentQueryDataContainer>())
       .Returns(homePage);

    _contentQueryExecutor
        .GetWebPageResult(
            builder: Arg.Any<ContentItemQueryBuilder>(),
            resultSelector: _webPageQueryResultMapper.Map<HomePage>,
            options: Arg.Any<ContentQueryExecutionOptions>(),
            cancellationToken: Arg.Any<CancellationToken>()
        )
        .Returns([homePage]);

    var testBuilder = new ContentItemQueryBuilder().InLanguage("en");
    HomePage mappedResult = _webPageQueryResultMapper.Map<HomePage>(null);
    IEnumerable<HomePage> result = await _contentQueryExecutor
        .GetWebPageResult(
            builder: testBuilder,
            resultSelector: _webPageQueryResultMapper.Map<HomePage>,
            options: null,
            cancellationToken: CancellationToken.None
        );
}

When I debug the test method I see that the value in the mappedResult variable is correct, it returns an instance of HomePage. However the result variable is an empty enumerable. That's the issue that I've been having, that it's just difficult to mock the IContentQueryExecutor.

I've also tried this out in v28.4.1, but it yields the same result. It could genuinely be the case that using xUnit is what's causing this problem. I'm curious to see whether this actually works in NUnit.


To answer this question, you have to login first.