Xperience unit test mocking of content query executor
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
@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?
@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.