Как вы издеваетесь над ILogger LogInformation

У меня есть класс, который получает ILogger, и я хочу смоделировать вызовы LogInformation, но это метод расширения. Как мне сделать соответствующий вызов для установки?

9 ответов

Если вы используете Moq >= 4.13, вот способ издеваться над ILogger:

logger.Verify(x => x.Log(
    It.IsAny<LogLevel>(),
    It.IsAny<EventId>(),
    It.IsAny<It.IsAnyType>(),
    It.IsAny<Exception>(),
    (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));

Вы можете изменить It.IsAny<LogLevel>(), It.IsAny<EventId>(), а также It.IsAny<Exception>() заглушки, чтобы быть более конкретными, но используя It.IsAnyType необходимо, потому что FormattedLogValues сейчас internal.

Ссылка: TState в ILogger.Log раньше был объектом, теперь FormattedLogValues

Пример с обратным вызовом, протестированный с Moq 4.14.5. Дополнительная информация доступна по этому вопросу Github

logger.Setup(x => x.Log(
    It.IsAny<LogLevel>(),
    It.IsAny<EventId>(),
    It.IsAny<It.IsAnyType>(),
    It.IsAny<Exception>(),
    (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()))
    .Callback(new InvocationAction(invocation =>
    {
        var logLevel = (LogLevel)invocation.Arguments[0]; // The first two will always be whatever is specified in the setup above
        var eventId = (EventId)invocation.Arguments[1];  // so I'm not sure you would ever want to actually use them
        var state = invocation.Arguments[2];
        var exception = (Exception?)invocation.Arguments[3];
        var formatter = invocation.Arguments[4];

        var invokeMethod = formatter.GetType().GetMethod("Invoke");
        var logMessage = (string?)invokeMethod?.Invoke(formatter, new[] { state, exception });

    }));

ILogger обычно используется через методы расширения, LogWarning, LogError и т. Д.

В моем случае меня заинтересовал метод LogWarning, который после просмотра кода вызывает метод Log из ILogger. Для того, чтобы поиздеваться над Moq, я сделал следующее:

 var list = new List<string>();
            var logger = new Mock<ILogger>();
            logger
                .Setup(l => l.Log<FormattedLogValues>(LogLevel.Warning, It.IsAny<EventId>(), It.IsAny<FormattedLogValues>(), It.IsAny<Exception>(), It.IsAny<Func<FormattedLogValues, Exception, string>>()))
                .Callback(
                delegate (LogLevel logLevel, EventId eventId, FormattedLogValues state, Exception exception, Func<FormattedLogValues, Exception, string> formatter)
                {
                    list.Add(state.ToString());
                });

Это тот случай, когда тестовый двойной класс может быть проще, чем Moq. Это немного больше работы для создания, но тогда вы можете повторно использовать его навсегда, и его легче читать и работать, чем обратный вызов Moq. (Мне нравится Moq, но не там, где есть более простой способ.)

В большинстве случаев это будет работать как есть, или вы можете настроить его.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;

public class LoggerDouble<T> : ILogger, ILogger<T>
{
    public List<LogEntry> LogEntries { get; } = new List<LogEntry>();

    // Add more of these if they make life easier.
    public IEnumerable<LogEntry> InformationEntries =>
        LogEntries.Where(e => e.LogLevel == LogLevel.Information);

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        LogEntries.Add(new LogEntry(logLevel, eventId, state, exception));
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }

    public IDisposable BeginScope<TState>(TState state)
    {
        return new LoggingScope();
    }

    public class LoggingScope : IDisposable
    {
        public void Dispose()
        {
        }
    }
}

public class LogEntry
{
    public LogEntry(LogLevel logLevel, EventId eventId, object state, Exception exception)
    {
        LogLevel = logLevel;
        EventId = eventId;
        State = state;
        Exception = exception;
    }

    public LogLevel LogLevel { get; }
    public EventId EventId { get; }
    public object State { get; }
    public Exception Exception { get; }
}

Создайте экземпляр и вставьте его в свой тестовый класс в качестве регистратора. Затем вы можете посмотреть на объекты в LogEntries коллекция, чтобы увидеть, что было зарегистрировано.

Тип State обычно будет FormattedLogValues, но ты можешь позвонить State.ToString() и просто получите строковое значение.

      logger.Verify(x => x.Log(
       LogLevel.Information, //Change LogLevel as required
       It.IsAny<EventId>(),
       It.Is<It.IsAnyType>((object v, Type _) => 
       v.ToString().Contains("MessageToVerify")), // Change MessageToVerify as required
       It.IsAny<Exception>(),
       (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));

Вот как я могу обойти для Moq (v4.10.1) Framework.

public static class TestHelper
{ 

    public static Mock<ILogger<T>> GetMockedLoggerWithAutoSetup<T>()
    {
        var logger = new Mock<ILogger<T>>();

        logger.Setup<object>(x => x.Log(
       It.IsAny<LogLevel>(),
       It.IsAny<EventId>(),
       It.IsAny<object>(),
       It.IsAny<Exception>(),
       It.IsAny<Func<object, Exception, string>>()));

        return logger;
    }

    public static void VerifyLogMessage<T>(Mock<ILogger<T>> mockedLogger, LogLevel logLevel, Func<string, bool> predicate, Func<Times> times)
    {
        mockedLogger.Verify(x => x.Log(logLevel, 0, It.Is<object>(p => predicate(p.ToString())), null, It.IsAny<Func<object, Exception, string>>()), times);
    }
}

-

public class Dummy
{

}

[Fact]
public void Should_Mock_Logger()
{
    var logger = TestHelper.GetMockedLoggerWithAutoSetup<Dummy>();
    logger.Object.LogInformation("test");
    TestHelper.VerifyLogMessage<Dummy>(logger, LogLevel.Information, msg => msg == "test", Times.Once);
}

-

Дело в том,

Если бы я выбрал любой другой <TCustom> чем <object> за logger.Setup()это не получится Verify шаг, сказав, что 0 звонки были сделаны для x.Log<TCustom> и показывает звонок сделан x.Log<object>, Так что я настроил мой общий логгер, чтобы издеваться Log<object>(..) метод вместо.

//Hi you can find the code with Moq.dll from the below link
http://www.dotnetsurfers.com/blog/2010/04/02/getting-started-with-mocking-part-2-using-moq

//Here Define Interfaces with models Logger,Product and ShoppingCart

using System;
using System.Diagnostics;
using System.IO;

namespace MOQSamples.Model
{
    public interface ILogger
    {
        void Log(string text);
    }

    public class Logger : ILogger
    {
        public void Log(string text)
        {
            TextWriter tw = new StreamWriter(@"C:\temp\moq.log",false);
            tw.WriteLine(text);
            tw.Close();
        }
    }
}




using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MOQSamples.Model
{
    public class ShoppingCart
    {
        public ShoppingCart(ILogger logger)
        {
            this._logger = logger;
        }

        private ILogger _logger;
        public decimal Total { get; set; }

        public void AddProduct(IProduct product)
        {
            Total = Total + product.Price;
            if (_logger != null)
                _logger.Log(String.Format("Product {0} has been added.",product.Name));
        }
    }
}





using System;
using System.Data.SqlClient;

namespace MOQSamples.Model
{
    public interface IProduct
    {
        string Name { get; set; }

        decimal Price { get; set; }

        string GetProductCategory();

    }

    public class Product : IProduct
    {
        public int ID {get;set;}

        public string Name {get; set;}
        public decimal Price
        {
            get { return GetPriceFromDatabase(); }
            set { throw new NotImplementedException(); }
        }

        public string GetProductCategory()
        {
            throw new NotImplementedException();
        }

        private decimal GetPriceFromDatabase()
        {
            #region Retrieve Price from DB

            var conn = new SqlConnection("Server=WIN-V0L52BJTJS6; Database=MOQ; Integrated Security=SSPI;");
            var query = "select Price from Product where ID =" + ID;
            var cmd = new SqlCommand(query, conn);
            conn.Open();
            var price = (decimal)cmd.ExecuteScalar();
            conn.Close();
            return price;

            #endregion
        }
    }
}

//testing the logger 


using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Moq.Language;
using MOQSamples.Model;


namespace MOQSamples.Test
{
    [TestClass]
    public class MOQDemoTests
    {
        private Mock<IProduct> _mockProduct;
        private Mock<ILogger> _mockLogger;

        [TestInitialize]
        public void InitializeTests()
        {
            _mockProduct = new Mock<IProduct>();
            _mockLogger = new Mock<ILogger>();
        }

        [TestMethod]
        public void Demo_Setup_Method()
        {
            //Show how a method call can be mocked and return fake data
            _mockProduct.Setup(m => m.GetProductCategory()).Returns("Test Category");
            Console.WriteLine(_mockProduct.Object.GetProductCategory());
        
        }

        [TestMethod]
        public void Demo_Setup_PropertyGet()
        {
            //Show how a property can be mocked and return fake data
            _mockProduct.SetupGet(m => m.Name).Returns("Product 1");
            Console.WriteLine(_mockProduct.Object.Name);
        }

        [TestMethod]
        [ExpectedException(typeof(ArgumentNullException))]
        public void Demo_Setup_ThrowException()
        {
            //show how a mock can be used to throw exception
            _mockLogger.Setup(m => m.Log(It.Is<string>(p => p == null))).
                Throws(new ArgumentNullException());
            _mockLogger.Object.Log(null);
        }

        [TestMethod]
        public void Demo_Validate_Params()
        {
            //show how mock can validate parameters
            _mockLogger.Setup(m => m.Log(It.IsRegex("[1-9]+"))).
                Callback(() => Console.WriteLine("Numbers passed"));
            _mockLogger.Object.Log("123");
        }

        [TestMethod]
        public void Demo_Verify_Interactions()
        {
            _mockLogger.Object.Log("test");
            _mockLogger.Verify(m => m.Log(It.Is<string>(s=>s=="test")),Times.Once());
        }

        [TestMethod]
        public void Demo_Setup_CallBack()
        {

            //show how a mock can be used to invoke a callback
            int counter = 0;
            _mockLogger.Setup(m => m.Log(It.IsAny<String>())).Callback(() => counter++);
            _mockLogger.Object.Log("test");
            _mockLogger.Object.Log("test2");

            Console.WriteLine("Counter is " + counter);
        }

    }
}

Очень помог ответ @ live2 выше. Просто завершил его, превратив в класс, который можно использовать для проверок;

      public class MockedLogger<T>
{
    public MockedLogger()
    {
        Mock = new Mock<ILogger<T>>();
        Mock.Setup(x => x.Log(
                It.IsAny<LogLevel>(),
                It.IsAny<EventId>(),
                It.IsAny<It.IsAnyType>(),
                It.IsAny<Exception>(),
                (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()))
            .Callback(new InvocationAction(invocation =>
            {
                var logLevel = (LogLevel)invocation.Arguments[0];
                var eventId = (EventId)invocation.Arguments[1];
                var state = invocation.Arguments[2];
                var exception = (Exception)invocation.Arguments[3];
                var formatter = invocation.Arguments[4];

                var invokeMethod = formatter.GetType().GetMethod("Invoke");
                var logMessage = (string)invokeMethod?.Invoke(formatter, new[] { state, exception });

                LoggedMessages.Add((logLevel, logMessage));
            }));
    }

    public Mock<ILogger<T>> Mock { get; }
    public List<(LogLevel Level, string Message)> LoggedMessages { get; } = new List<(LogLevel Level, string Message)>();
}

Использование в модульном тесте ( NUnit с участием FluentAssertions);

      [TestFixture]
public class When_doing_something
{
    private readonly MockedLogger<ClassUnderTest> _mockedLogger = new MockedLogger<ClassUnderTest>();

    [OneTimeSetUp]
    public async Task Initialize()
    {
        await new ClassUnderTest(_mockedLogger.Mock.Object).DoSomething();
    }

    [Test]
    public void Then_the_operation_is_logged()
    {
        _mockedLogger.LoggedMessages.Should().Contain((LogLevel.Information, "expected log message"));
    }
}

Я вижу здесь множество очень тяжелых решений, поэтому поделюсь своим очень легким подходом.

Обычно я просто хочу убедиться, что когда что-то было вызвано, оно должно регистрировать x много раз. Поэтому в своих модульных тестах я просто делаю новую реализацию Logger<T> и использовать шаблон фасада, чтобы открыть общедоступное поле, которое я затем могу использовать для тестирования.

      private class VerifiableLogger : ILogger<T>//Replace T with your type
{
    public int calledCount { get; set; }

    //boiler plate, required to implement ILogger<T>
    IDisposable ILogger.BeginScope<TState>(TState state) => throw new NotImplementedException();
    bool ILogger.IsEnabled(LogLevel logLevel) => throw new NotImplementedException();

    //Meaningful method, this get's called when you use .LogInformation()
    void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) 
      =>  this.calledCount++;
}

Если вы хотите проверить и увидеть, какое было последнее сообщение журнала, или просмотреть все сообщения журнала, вы можете использовать тот же подход, который я использовал.

      private class VerifiableLogger : ILogger<T>
        {
            public int calledCount { get; set; }
            public List<string> logList { get; set; }

            IDisposable ILogger.BeginScope<TState>(TState state) => throw new NotImplementedException();
            bool ILogger.IsEnabled(LogLevel logLevel) => throw new NotImplementedException();
            void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) {
                this.calledCount++;
                if (state != null)
                {
                    this.logList.Add(state.ToString());
                }
            }
        }

Затем вы можете изучить результаты своих модульных тестов. Очень удобно проверять форматирование для целей структурированного ведения журнала, если вы этого хотите.

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