I have use CQRS pattern in my .NET 6 project. I have a CreateQueryCommandHandler
. Its access modifier is set to internal
which I cannot change. I am trying to unit test my command handlers.
I have made changes in my application project that contains my commands to give internal class access to my unit tests project. But simply adding this doesn't resolve the issue.
I have to also added
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=3483847384783478347.....
Without this line added to my create command handler class, I get this error:
System.ArgumentException : Can not create proxy for type Microsoft.Extensions.Logging.ILogger`1[[QueryStore.Application.Queries.CreateQueryCommandHandler, QueryStore.Application, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]] because type QueryStore.Application.Queries.CreateQueryCommandHandler is not accessible. Make it public, or internal and mark your assembly with [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000....")] attribute, because assembly Microsoft.Extensions.Logging.Abstractions is strong-named. (Parameter 'interfaceToProxy')
Stack Trace:
at Castle.DynamicProxy.DefaultProxyBuilder.AssertValidTypeForTarget(Type type, Type target, String paramName)
at Castle.DynamicProxy.DefaultProxyBuilder.AssertValidTypeForTarget(Type type, Type target, String paramName)
at Castle.DynamicProxy.DefaultProxyBuilder.AssertValidType(Type target, String paramName)
at Castle.DynamicProxy.DefaultProxyBuilder.CreateInterfaceProxyTypeWithoutTarget(Type interfaceToProxy, Type[] additionalInterfacesToProxy, ProxyGenerationOptions options) at Castle.DynamicProxy.ProxyGenerator.CreateInterfaceProxyTypeWithoutTarget(Type interfaceToProxy, Type[] additionalInterfacesToProxy, ProxyGenerationOptions options) at Castle.DynamicProxy.ProxyGenerator.CreateInterfaceProxyWithoutTarget(Type interfaceToProxy, Type[] additionalInterfacesToProxy, ProxyGenerationOptions options, IInterceptor[] interceptors) at Moq.CastleProxyFactory.CreateProxy(Type mockType, IInterceptor interceptor, Type[] interfaces, Object[] arguments) in //src/Moq/Interception/CastleProxyFactory.cs:line 98 at Moq.Mock1.InitializeInstance() in /_/src/Moq/Mock
1.cs:line 502 at Moq.Mock1.OnGetObject() in /_/src/Moq/Mock
1.cs:line 516 at Moq.Mock.get_Object() in //src/Moq/Mock.cs:line 180 at Moq.Mock1.get_Object() in /_/src/Moq/Mock
1.cs:line 453 at QueryStore.Application.UnitTests.Queries.Commands.CreateQueryCommandHandlerTests.Handle_Should_ReturnSuccessResult_WhenQueryIsAddedToDB() in /home/hardik/Desktop/queryhub/src/Application/tests/Queries/CreateQueryCommandHandlerTests.cs:line 127 at QueryStore.Application.UnitTests.Queries.Commands.CreateQueryCommandHandlerTests.Handle_Should_ReturnSuccessResult_WhenQueryIsAddedToDB() in /home/hardik/Desktop/queryhub/src/Application/tests/Queries/CreateQueryCommandHandlerTests.cs:line 133 at System.Threading.Tasks.Task.<>c.b__128_0(Object state)Failed QueryStore.Application.UnitTests.Queries.Commands.CreateQueryCommandHandlerTests.Handle_Should_ReturnErrorResult_WhenUnknownErrorOccurs [151 ms]
Error Message:
System.ArgumentException : Can not create proxy for type Microsoft.Extensions.Logging.ILogger`1[[QueryStore.Application.Queries.CreateQueryCommandHandler, QueryStore.Application, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]] because type QueryStore.Application.Queries.CreateQueryCommandHandler is not accessible. Make it public, or internal and mark your assembly with [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000....")] attribute, because assembly Microsoft.Extensions.Logging.Abstractions is strong-named. (Parameter 'interfaceToProxy')
All the test cases work correctly after adding Dynamic Proxy thing. I would appreciate if some one would explain me why this works. And if possible how to remove it. Making my code much cleaner. I would also appreciate better testing strategies.
My CommandHandler
code:
using FluentValidation;
using MediatR;
using Microsoft.Extensions.Logging;
using QueryStore.Application.Common;
using System.Data.Common;
namespace QueryStore.Application.Queries;
internal sealed class CreateQueryCommandHandler : IRequestHandler<CreateQueryCommand, Result<string>>
{
private readonly IQueryStoreDbContext _context;
private readonly IValidator<CreateQueryCommand> _validator;
private readonly ILogger _logger;
public CreateQueryCommandHandler(
IQueryStoreDbContext context,
IValidator<CreateQueryCommand> validator,
ILogger<CreateQueryCommandHandler> logger)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_validator = validator ?? throw new ArgumentNullException(nameof(validator));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<Result<string>> Handle(CreateQueryCommand request, CancellationToken cancellationToken)
{
var validationResult = await _validator.ValidateAsync(request, cancellationToken);
if (!validationResult.IsValid)
{
return Result.Error(validationResult.Errors.Select(e => new Error($"{e.PropertyName}", e.ErrorMessage)));
}
try
{
var entity = request.AsQuery();
_context.Queries.Add(entity);
await _context.SaveChangesAsync(cancellationToken: cancellationToken);
return Result<string>.Success(entity.Id);
}
catch (DbException ex)
{
_logger.LogError(ex, "An error occurred while saving query to database");
return Result.Error(new Error("error", ex.Message));
}
catch (Exception ex)
{
_logger.LogError(ex, "An unknown error occurred while saving query");
return Result.Error(new Error("error", ex.Message));
}
}
}
Unit test code:
using System.Data.Common;
using FluentAssertions;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using QueryStore.Application.Queries;
using QueryStore.Domain;
namespace QueryStore.Application.UnitTests.Queries.Commands;
public class CreateQueryCommandHandlerTests
{
private readonly Mock<IQueryStoreDbContext> _contextMock = new Mock<IQueryStoreDbContext>();
private readonly IValidator<CreateQueryCommand> _validator = new CreateQueryCommandValidator();
private readonly Mock<ILogger<CreateQueryCommandHandler>> _loggerMock =
new Mock<ILogger<CreateQueryCommandHandler>>();
[Fact]
public async void Handle_Should_ReturnSuccessResult_WhenQueryIsAddedToDB()
{
// Arrange
var command = new CreateQueryCommand()
{
Name = "Some Name",
Content = "Select * from AbcCompany",
Description = "Some description about the query.",
Tags = new[] { "company", "import" }
};
var options = new DbContextOptionsBuilder<QueryStoreDbContext>()
.UseInMemoryDatabase("TestDatabase")
.Options;
await using var dbContext = new QueryStoreDbContext(options);
// Act
var validationResult = await _validator.ValidateAsync(command);
var handler = new CreateQueryCommandHandler(dbContext, _validator, _loggerMock.Object);
var result = await handler.Handle(command, cancellationToken: CancellationToken.None);
// Assert
validationResult.IsValid.Should().BeTrue();
result.IsSuccess.Should().BeTrue();
}
[Fact]
public async Task Handle_Should_ReturnErrorResult_WhenUnknownErrorOccurs()
{
// Arrange
var command = new CreateQueryCommand()
{
Name = "Some Name",
Content = "Select * from AbcCompany",
Description = "Some description about the query.",
Tags = new[] { "company", "import" }
};
_contextMock.Setup(context =>
context.Queries.Add(It.IsAny<Query>())
).Throws(new Exception("Unknown error"));
// Act
var handler = new CreateQueryCommandHandler(_contextMock.Object, _validator, _loggerMock.Object);
var result = await handler.Handle(command, cancellationToken: CancellationToken.None);
// Assert
result.IsSuccess.Should().BeFalse();
result.Errors.Should().NotBeEmpty();
}
}
Moq
in the testsMoq
internally uses the Castle.DynamicProxy
library to generate mocksCastle.DynamicProxy
emits the code of the mocks in the runtime and place that code in the dynamically-created assembly called DynamicProxyGenAssembly2
CreateQueryCommandHandler
.As per your second question - I did not know the better testing approach which will not require that attribute.
You can also check out this question to see the other ways to achieve the internals-visible-to effect: How to do internal interfaces visible for Moq?