How do you mock ILogger LogInformation
Asked Answered
A

8

60

I have a class that receives an ILogger and I want to mock the LogInformation calls but this is an extension method. How do I make the appropiate setup call for this?

Avan answered 8/10, 2018 at 17:55 Comment(5)
Are you using any specific mocking tool?Ellipsis
Look at the source for these extensions. All the extensions eventually just call ILogger.Log() so if you implement / mock that you are good.T
Extension methods are static and not easily mocked. Related : #2296460. It sounds like total oversight on the original ILogger design to need an extension for Loginformation Imo.Croissant
If it is a extension, it will probably extend a specific class (Your class that implements Ilogger). You can create a Fake of that class and use it with the extension, to achieve the asserts you expect. You can even create "Fake" results of your class calls. Do a quick google research about C# Mocking frameworks, for example fakeiteasy.github.io;Phrenetic
Does this answer your question? How to unit test with ILogger in ASP.NET CoreFragonard
F
59

If you're using Moq >= 4.13, here is a way to mock 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>()));

You can change the It.IsAny<LogLevel>(), It.IsAny<EventId>(), and It.IsAny<Exception>() stubs to be more specific, but using It.IsAnyType is necessary because FormattedLogValues is now internal.

Reference: TState in ILogger.Log used to be object, now FormattedLogValues

Furore answered 16/10, 2019 at 12:55 Comment(6)
My kingdom if you also show the .Callback for a Setup !!Canice
Not working :: .Callback((Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, object t, Exception ex, Func<object, Exception, string> formatter) => {Canice
@ScottFraley I think you have the wrong signature. If you F12 / Go To Definition on ILogger, you do see a Log method that takes the parameter structure crgolden outlines.Equiponderate
Also It.IsAny<Func<It.IsAnyType, Exception, string>>() would work instead of (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()Sanjay
I found this solution: adamstorr.azurewebsites.net/blog/mocking-ilogger-with-moq. It describes a Moq only solution also verifying the message. loggerMock .Verify(l => l.Log( It.Is<LogLevel>(l => l == LogLevel.Information), It.IsAny<EventId>(), It.Is<It.IsAnyType>((v, t) => v.ToString() == "Message"), It.IsAny<Exception>(), It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.AtLeastOnce);Sanjay
@MarcelMelzig the best answer so farArgilliferous
S
38

Example with a Callback, tested with Moq 4.14.5. More Informations available on this Github Issue

PM> install-package Moq

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

A complete generic helper class for 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($"{logLevel} - {logMessage}");
            }));

        return logger;
    }
}
Suckling answered 2/9, 2020 at 8:49 Comment(2)
great post - adamstorr.azurewebsites.net/blog/mocking-ilogger-with-moqExtemporaneous
It's a very ingenious solution, btw I'm facing a problem when trying to do structured logging; the logMessage unfortunately fails to create a correctly formatted string when passing a parameter a complex object. It prints only the namespace of the object like "the value is Project.Domain.MyClass"Oran
A
14

ILogger is normally used thru extension methods, LogWarning, LogError, etc.

In my case I was interested in the LogWarning method which after looking at the code calls the Log method from ILogger. In order to mock it with Moq, this is what I ended up doing:

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

In newer versions of .NET Core 3.0 this won't work. Because FormattedLogValues is an internal type. You need to update the moq version to at least:

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

After updating Moq The workaround is like this:

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

Notice the special cast: (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()) and also the IInvocation to handle the arguments.

Avan answered 10/10, 2018 at 12:41 Comment(4)
I used dependency Injection _mockLogger = new Mock<ILogger>();Julianajuliane
you define global private Mock<ILogger> _logger then in the constructor assign it and in the class where ever call like you display in the codeJulianajuliane
Oh man. I really wanted this to work. Maybe it is my version.. but I now get : "FormattedLogValues is inaccessible due to its protection level" :( This is my sad face. FYI I have " <PackageReference Include="Moq" Version="4.13.1" />" Maybe consider posting your version of Moq if you get a chance (to maybe see where the cutoff is) Thanks.Canice
@granaCoder better late than never. I have updated my code. That works with the moq 4.18Avan
A
13

This is a case where a "fake" class may be easier than Moq. It's a little more work to create, but then you can re-use it forever and it's easier to read and work with than a Moq callback. (I like Moq, but not when there's an easier way.)

For most use cases this will work as-is, or you can tweak it.

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

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

    public IEnumerable<LogEntry> InformationEntries =>
        LogEntries.Where(e => e.LogLevel == LogLevel.Information);

    public IEnumerable<LogEntry> WarningEntries =>
        LogEntries.Where(e => e.LogLevel == LogLevel.Warning);

    public IEnumerable<LogEntry> ErrorEntries =>
        LogEntries.Where(e => e.LogLevel == LogLevel.Error);

    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; }
    public string Message => State?.ToString() ?? string.Empty;
    public Dictionary<string, object> Params => (State as IReadOnlyList<KeyValuePair<string, object?>>)!.ToDictionary(key => key.Key, value => value.Value)!;
    public string FormatString => (string)Params["{OriginalFormat}"];
}

Create an instance and inject it into your test class as the logger. Then you can look at the objects in the LogEntries collection to see what got logged.

The type of State will typically be FormattedLogValues, but you can call State.ToString() and just get the string value.

Accusal answered 30/10, 2020 at 21:42 Comment(2)
Easiest way, honestly. Tried a few others listed here, had issues with them. It's easy to forget that sometimes a fake is better than a mock. Smart thinking.Lebna
This is one of the few cases where I end up searching for my own answer and pasting the code over and over.Accusal
S
9

If you have a logger (new Mock<ILogger<ReplaceWithYourObject>>()) mock injected in ctor. Then this should help to verify log message and log level.

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>()));
Sissel answered 4/4, 2022 at 13:0 Comment(0)
P
5

This is how I workaround for 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);
}

--

The thing is,

If I had chosen any other <TCustom> than <object> for logger.Setup(), it would fail on Verify step saying that 0 calls were made for x.Log<TCustom> and showing a call made to x.Log<object>. So I setup my generic logger to mock Log<object>(..) method instead.

Putty answered 26/2, 2019 at 14:35 Comment(1)
Hu, seems to have broken recentlyHedley
B
1

Was greatly helped by @live2's answer above. Just completed it by making it into a class that can be used for verifications;

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

Usage in unit test (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"));
    }
}
Bulimia answered 24/9, 2021 at 8:27 Comment(0)
T
0

I am seeing lots of very heavy solutions here, so I will share my very lightweight approach.

Normally, I just want to verify that when something was called, it should log x many times. So in my unit tests I just make a new implementaion of Logger<T> and use the facade pattern to expose a public field which I can then use for testing.

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

If you wanted to check and see what the last log message was, or to see all of the log messages, you could use this similar approach I've also used.

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

Then you can dig into the results in your unit tests. Very good to verify formatting for structured logging purposes, if you wanted to do so.

enter image description here

Triciatrick answered 22/9, 2021 at 14:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.