Как вы издеваетесь над 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());
}
}
}
Затем вы можете изучить результаты своих модульных тестов. Очень удобно проверять форматирование для целей структурированного ведения журнала, если вы этого хотите.