簡體   English   中英

你如何模擬 ILogger LogInformation

[英]How do you mock ILogger LogInformation

我有一個接收 ILogger 的 class 並且我想模擬 LogInformation 調用,但這是一種擴展方法。 如何為此進行適當的設置調用?

如果您使用的是 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

參考: ILogger.Log 中的 TState 以前是對象,現在是 FormattedLogValues

帶有回調的示例,使用 Moq 4.14.5 進行測試。 有關此Github 問題的更多信息

var logger = new Mock<ILogger<ReplaceWithYourObject>>();

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 });
    }));

一個完整的 UnitTesting 通用輔助類

public static class LoggerHelper
{
    public static Mock<ILogger<T>> GetLogger<T>()
    {
        var logger = new Mock<ILogger<T>>();

        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 });

                Trace.WriteLine(logMessage);
            }));

        return logger;
    }
}

ILogger 通常通過擴展方法、LogWarning、LogError 等使用。

就我而言,我對 LogWarning 方法感興趣,在查看代碼后,該方法從 ILogger 調用 Log 方法。 為了用 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());
                    });

在較新版本的 .NET Core 3.0 中,這不起作用。 因為 FormattedLogValues 是內部類型。 您需要將最小起訂量版本更新為至少:

`<PackageReference Include="Moq" Version="4.16.0" />`

更新Moq的解決方法是這樣的:

            var log = new List<string>();
            var mockLogger = new Mock<ILogger>();
            mockLogger.Setup(
                l => l.Log(
                    It.IsAny<LogLevel>(),
                    It.IsAny<EventId>(),
                    It.IsAny<It.IsAnyType>(),
                    It.IsAny<Exception>(),
                    (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()))
                .Callback((IInvocation invocation) =>
                {
                    var logLevel = (LogLevel)invocation.Arguments[0];
                    var eventId = (EventId)invocation.Arguments[1];
                    var state = (IReadOnlyCollection<KeyValuePair<string, object>>)invocation.Arguments[2];
                    var exception = invocation.Arguments[3] as Exception;
                    var formatter = invocation.Arguments[4] as Delegate;
                    var formatterStr = formatter.DynamicInvoke(state, exception);
                    log.Add(
                      $"{logLevel} - {eventId.Id} - Testing - {formatterStr}");
                });

請注意特殊類型轉換: (Func<It.IsAnyType, Exception, string>)It.IsAny<object>())以及用於處理參數的 IInvocation。

這就是我對Moq (v4.10.1) 框架的解決方法。

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);
}

——

事情是,

如果我為logger.Setup()選擇了除<object> <TCustom>之外的任何其他<TCustom> ,它會在Verify步驟失敗,說對x.Log<TCustom>進行了0 次調用並顯示對x.Log<object>的調用x.Log<object> . 因此,我將通用記錄器設置為模擬Log<object>(..)方法。

如果你有一個loggernew Mock<ILogger<ReplaceWithYourObject>>() )模擬注入ctor。 那么這應該有助於驗證日志消息和日志級別。

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 更容易。 創建需要更多的工作,但是您可以永遠重復使用它,並且它比起訂量回調更易於閱讀和使用。 (我喜歡最小起訂量,但不喜歡有更簡單的方法。)

對於大多數用例,這將按原樣工作,或者您可以對其進行調整。

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()並獲取字符串值。

 //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); } } } 

我在這里看到了很多非常重的解決方案,所以我將分享我的非常輕量級的方法。

通常,我只想驗證當某個東西被調用時,它應該多次記錄 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());
                }
            }
        }

然后,您可以深入了解單元測試中的結果。 如果您願意,可以很好地驗證用於結構化日志記錄的格式。

在此處輸入圖片說明

上面@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 with 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"));
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM