Search code examples
c#.netunit-testingmocking

I am trying to test a command handler in .NET 6. I had to add DynamicProxyGenAssembly2 attribute. How to remove it?


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/Mock1.cs:line 502 at Moq.Mock1.OnGetObject() in /_/src/Moq/Mock1.cs:line 516 at Moq.Mock.get_Object() in //src/Moq/Mock.cs:line 180 at Moq.Mock1.get_Object() in /_/src/Moq/Mock1.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();
    }   
}

Solution

    • You are using Moq in the tests
    • Moq internally uses the Castle.DynamicProxy library to generate mocks
    • Castle.DynamicProxy emits the code of the mocks in the runtime and place that code in the dynamically-created assembly called DynamicProxyGenAssembly2
    • that runtime-emitted code should be able to access the original entities you are mocking, including CreateQueryCommandHandler.
    • So, to make it work you need to add the mentioned attribute to allow the dynamically-generated code to access the internal members.

    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?