Модульное тестирование ASP.NET - Mocking ILogger в модульных тестах

В настоящее время я шучу над ILogger в моем контроллере Unit Test Controller со следующим кодом:

private readonly Mock<ILogger> _logger = new Mock<ILogger>();

Мне нужно использовать этот Mock ILogger для регистрации сгенерированных исключений, которые утверждаются в различных модульных тестах.

Например:

    [Test]
    public void Arguments_CallBoardStatsRepo_Null()
    {
        Assert.Throws<NullReferenceException>(() => new AgentsController(null, _clientCallsRepoMock.Object, _agentStatusRepoMock.Object, _logger.Object));
       _logger.Verify(m => m.Error("Error", It.IsAny<NullReferenceException>()), Times.Once);

    }

Мне нужно добавить проверку ArgumentNullException для поддельного регистратора (_logger).

Каков будет лучший способ сделать это?

РЕДАКТИРОВАТЬ: контроллер проверяется

public class AgentsController : ApiController
{
    readonly IAgentStatusRepo _agentStatusRepo;
    readonly ICallBoardStatsRepo _callBoardRepo;
    readonly IClientCallsRepo _clientCallRepo;
    readonly ILogger _logger;

    public AgentsController(ICallBoardStatsRepo callBoardRepo,
        IClientCallsRepo clientCallRepo,
        IAgentStatusRepo agentStatusRepo,
        ILogger logger)
    {
        Util.Guard.ArgumentsAreNotNull(callBoardRepo, clientCallRepo, agentStatusRepo);

        _callBoardRepo = callBoardRepo;
        _clientCallRepo = clientCallRepo;
        _agentStatusRepo = agentStatusRepo;
        _logger = logger;
    }

    [HttpGet]
    [Route("api/agents")]
    public IHttpActionResult FindAllAgentsByClientGroup(string group)
    {
        IEnumerable<AgentStatus> agentCallStats = _agentStatusRepo.ByGroupKey(group).ToList();
        return Ok(agentCallStats);
    }
}

1 ответ

По моим наблюдениям, лугер даже не используется в показанном тестируемом классе, поэтому проверять на макете нечего.

Обратите внимание, что Assert.Throws возвращает выброшенное исключение, чтобы вы могли

var exception = 

[Test]
public void Arguments_CallBoardStatsRepo_Null() {
    //Act
    Action act = () => new AgentsController(null, _clientCallsRepoMock.Object, _agentStatusRepoMock.Object, _logger.Object);

    //Assert
    ArgumentNullException exception = Assert.Throws<ArgumentNullException>(act);

    //...inspect exception as desired.
}

В общем, вы должны полагаться на DI как в тестах, так и во время выполнения. Следующая библиотека содержит регистратор тестов, который вы можете использовать в тестах: https://www.nuget.org/packages/com.github.akovac35.Logging.Testing/

Примеры использования доступны здесь: https://github.com/akovac35/Logging.Samples

Отказ от ответственности: я являюсь автором вышеизложенного.

Регистратор тестов отправляет записи журнала в тестовый приемник, который вы можете запросить для конкретных сообщений, аналогично следующему фрагменту кода:

[Test]
public void Test_WithLoggingToTestConsole_Works()
{
    // The service provider should be defined on per-test level or logger writes will accumulate and may result in OOM - clean them with testSink.Clear()
    var serviceProvider = serviceCollection.BuildServiceProvider();
    var controller = serviceProvider.GetRequiredService<AgentsController>();
    controller.Invoke();

    var testSink = serviceProvider.GetRequiredService<ITestSink>();

    Assert.IsTrue(testSink.Writes.Count > 0);
    Assert.IsTrue(testSink.Scopes.Count > 0);
}

Пример настройки:

По умолчанию использовать NullLogger:

public class AgentsController: ControllerBase
{        
    private ILogger _logger = NullLogger.Instance;
    
    protected AgentsController(ILogger<AgentsController> logger = null)
    {
        if (logger != null) _logger = logger;
    }
}

Теперь подключите тесты:

using com.github.akovac35.Logging.Testing;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Shared.Mocks;
using System;

namespace TestApp
{
    [TestFixture]
    public class TestLoggingExamples
    {
        [OneTimeSetUp]
        public void OneTimeSetUp()
        {
            customOnWrite = writeContext => {
                Console.WriteLine(writeContext);
            };

            customOnBeginScope = scopeContext => {
                Console.WriteLine(scopeContext);
            };

            serviceCollection = new ServiceCollection();
            serviceCollection.AddTransient(typeof(AgentsController));
            // Register mocks as you would any other service ...

            // Register TestLogger using extension method
            serviceCollection.AddTestLogger(onWrite: customOnWrite, onBeginScope: customOnBeginScope);
        }

        private IServiceCollection serviceCollection;

        private Action<WriteContext> customOnWrite;
        private Action<ScopeContext> customOnBeginScope;

        [Test]
        public void Test_WithLoggingToTestConsole_Works()
        {
            // The service provider should be defined on per-test level or logger writes will accumulate and may result in OOM - clean them with testSink.Clear()
            var serviceProvider = serviceCollection.BuildServiceProvider();
            var controller = serviceProvider.GetRequiredService<AgentsController>();
            controller.Invoke();

            var testSink = serviceProvider.GetRequiredService<ITestSink>();

            Assert.IsTrue(testSink.Writes.Count > 0);
            Assert.IsTrue(testSink.Scopes.Count > 0);
        }
    }
}

testSink.Writes содержит объекты WriteContext, в которых могут быть утверждены определенные сообщения журнала:

using Microsoft.Extensions.Logging;
using System;

namespace com.github.akovac35.Logging.Testing
{
    [System.Diagnostics.DebuggerDisplay("{ToString()}")]
    public class WriteContext
    {
        public LogLevel LogLevel { get; set; }

        public EventId EventId { get; set; }

        public object State { get; set; }

        public Exception Exception { get; set; }

        public Func<object, Exception, string> Formatter { get; set; }

        public ScopeContext Scope { get; set; }

        public ILogger Logger { get; set; }

        public DateTime Timestamp { get; set; }

        public int ThreadId { get; set; }

        public virtual string Message
        {
            get
            {
                return Formatter(State, Exception);
            }
        }

        public override string ToString()
        {
            return $"[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] {ThreadId} {LogLevel} EventId: {EventId}{(Scope != null ? $" Scope: <{Scope}>" : "")} Message: {Message}{(Exception != null ? $"{Environment.NewLine}{Exception}" : "")}";
        }
    }
}

Вот статья на эту тему. В нем рассказывается о том, как имитировать и проверять вызовы ILogger с помощью Moq. Вот простой пример:

        _logTest.Process();
        _loggerMock.Verify(l => l.Log(
            LogLevel.Information,
            It.IsAny<EventId>(),
            It.IsAny<It.IsAnyType>(),
            It.IsAny<Exception>(),
            (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()), Times.Exactly(1));

А вот более сложный пример проверки конкретных деталей звонка.

        _loggerMock.Verify
        (
            l => l.Log
            (
                //Check the severity level
                LogLevel.Error,
                //This may or may not be relevant to your scenario
                It.IsAny<EventId>(),
                //This is the magical Moq code that exposes internal log processing from the extension methods
                It.Is<It.IsAnyType>((state, t) =>
                    //This confirms that the correct log message was sent to the logger. {OriginalFormat} should match the value passed to the logger
                    //Note: messages should be retrieved from a service that will probably store the strings in a resource file
                    CheckValue(state, LogTest.ErrorMessage, "{OriginalFormat}") &&
                    //This confirms that an argument with a key of "recordId" was sent with the correct value
                    //In Application Insights, this will turn up in Custom Dimensions
                    CheckValue(state, recordId, nameof(recordId))
            ),
            //Confirm the exception type
            It.IsAny<ArgumentNullException>(),
            //Accept any valid Func here. The Func is specified by the extension methods
            (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),
            //Make sure the message was logged the correct number of times
            Times.Exactly(1)
        );

Обратите внимание, что вы можете проверить тип исключения выше

Другие вопросы по тегам