244

This is my controller:

public class BlogController : Controller
{
    private IDAO<Blog> _blogDAO;
    private readonly ILogger<BlogController> _logger;

    public BlogController(ILogger<BlogController> logger, IDAO<Blog> blogDAO)
    {
        this._blogDAO = blogDAO;
        this._logger = logger;
    }
    public IActionResult Index()
    {
        var blogs = this._blogDAO.GetMany();
        this._logger.LogInformation("Index page say hello", new object[0]);
        return View(blogs);
    }
}

As you can see I have 2 dependencies, a IDAO and a ILogger

And this is my test class, I use xUnit to test and Moq to create mock and stub, I can mock DAO easy, but with the ILogger I don't know what to do so I just pass null and comment out the call to log in controller when run test. Is there a way to test but still keep the logger somehow ?

public class BlogControllerTest
{
    [Fact]
    public void Index_ReturnAViewResult_WithAListOfBlog()
    {
        var mockRepo = new Mock<IDAO<Blog>>();
        mockRepo.Setup(repo => repo.GetMany(null)).Returns(GetListBlog());
        var controller = new BlogController(null,mockRepo.Object);

        var result = controller.Index();

        var viewResult = Assert.IsType<ViewResult>(result);
        var model = Assert.IsAssignableFrom<IEnumerable<Blog>>(viewResult.ViewData.Model);
        Assert.Equal(2, model.Count());
    }
}
BozoJoe
  • 6,117
  • 4
  • 44
  • 66
duc
  • 2,585
  • 2
  • 14
  • 14
  • 2
    You can use a mock as a stub, as Ilya suggests, if you're not actually trying to test that the logging method itself was called. If that's the case, mocking the logger doesn't work, and you can try a few different approaches. I've written a [short article](https://ardalis.com/testing-logging-in-aspnet-core) showing a variety of approaches. The article includes [a full GitHub repo with each of the different options](https://github.com/ardalis/TestingLogging). In the end, my recommendation is to use your own adapter rather than working directly with the ILogger type, if you need to be able to – ssmith Aug 09 '17 at 17:29
  • As @ssmith mentioned there are some troubles with verifying actual calls for `ILogger`. He has some good suggestions in his blogpost and I have come with my solution that seems to solve most of the troubles in the [answer below](https://stackoverflow.com/a/54809607/1671558). – Ilya Chernomordik Feb 21 '19 at 14:46

17 Answers17

276

Just mock it as well as any other dependency:

var mock = new Mock<ILogger<BlogController>>();
ILogger<BlogController> logger = mock.Object;

var controller = new BlogController(logger);

or use this short equivalent:

logger = Mock.Of<ILogger<BlogController>>()

You probably will need to install Microsoft.Extensions.Logging.Abstractions package to use ILogger<T>.

Moreover, you can create a real logger:

var serviceProvider = new ServiceCollection()
    .AddLogging()
    .BuildServiceProvider();

var factory = serviceProvider.GetService<ILoggerFactory>();

var logger = factory.CreateLogger<BlogController>();
Ilya Chumakov
  • 23,161
  • 9
  • 86
  • 114
  • 7
    to log to the debug output window call AddDebug() on the factory: var factory = serviceProvider.GetService().AddDebug(); – spottedmahn Aug 06 '17 at 17:20
  • 14
    I found the "real logger" approach more effective! – DanielV Jul 04 '18 at 07:46
  • 1
    The real logger part also works great for testing the LogConfiguration and LogLevel in specific scenarios. – Martin Lottering Nov 23 '18 at 08:31
  • 1
    This approach will only allow stub, but not verification of calls. I have come with my solution that seems to solve most of the troubles with verification in the [answer below](https://stackoverflow.com/a/54809607/1671558). – Ilya Chernomordik Mar 07 '19 at 15:40
  • 6
    Since .net 5, `AddDebug` should be called in `AddLogging` instead from `ILoggerFactory`. `new ServiceCollection().AddLogging(builder => builder.AddDebug())...` – Cyril Durand May 16 '21 at 10:33
  • You've missed Dispose calls. – SerG May 18 '21 at 22:31
  • 2
    For ``AddDebug`` you'll need to include https://www.nuget.org/packages/Microsoft.Extensions.Logging.Debug/ – Jack Miller Apr 22 '22 at 06:24
146

Actually, I've found Microsoft.Extensions.Logging.Abstractions.NullLogger<> which looks like a perfect solution. Install the package Microsoft.Extensions.Logging.Abstractions, then follow the example to configure and use it:

using Microsoft.Extensions.Logging;

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddSingleton<ILoggerFactory, NullLoggerFactory>();

    ...
}
using Microsoft.Extensions.Logging;

public class MyClass : IMyClass
{
    public const string ErrorMessageILoggerFactoryIsNull = "ILoggerFactory is null";

    private readonly ILogger<MyClass> logger;

    public MyClass(ILoggerFactory loggerFactory)
    {
        if (null == loggerFactory)
        {
            throw new ArgumentNullException(ErrorMessageILoggerFactoryIsNull, (Exception)null);
        }

        this.logger = loggerFactory.CreateLogger<MyClass>();
    }
}

and unit test

//using Microsoft.VisualStudio.TestTools.UnitTesting;
//using Microsoft.Extensions.Logging;

[TestMethod]
public void SampleTest()
{
    ILoggerFactory doesntDoMuch = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory();
    IMyClass testItem = new MyClass(doesntDoMuch);
    Assert.IsNotNull(testItem);
}   
Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
Amir Shitrit
  • 1,663
  • 2
  • 11
  • 7
121

UPDATE (thanks @Gopal Krishnan for the comment):

With Moq >= 4.15.0 the following code is working (the cast is no longer needed):

 loggerMock.Verify(
                x => x.Log(
                    LogLevel.Information,
                    It.IsAny<EventId>(),
                    It.Is<It.IsAnyType>((o, t) => string.Equals("Index page say hello", o.ToString(), StringComparison.InvariantCultureIgnoreCase)),
                    It.IsAny<Exception>(),
                    It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
                Times.Once);

Previous version of the answer (for Moq < 4.15.0):

For .net core 3 answers that are using Moq

are no longer working due to a change described in the issue TState in ILogger.Log used to be object, now FormattedLogValues

Luckily stakx provided a nice workaround. So I'm posting it in hope it can save time for others (it took a while to figure the things out):

 loggerMock.Verify(
                x => x.Log(
                    LogLevel.Information,
                    It.IsAny<EventId>(),
                    It.Is<It.IsAnyType>((o, t) => string.Equals("Index page say hello", o.ToString(), StringComparison.InvariantCultureIgnoreCase)),
                    It.IsAny<Exception>(),
                    (Func<It.IsAnyType, Exception, string>) It.IsAny<object>()),
                Times.Once);
Tono Nam
  • 34,064
  • 78
  • 298
  • 470
Ivan Samygin
  • 4,210
  • 1
  • 20
  • 33
  • I expected to be able to replace `(Func) It.IsAny())` with `It.IsAny>())`, but it doesnt work. What is this magic :-)? – Gopal Krishnan Jan 18 '21 at 13:43
  • 1
    Loooks like I just need to update to latest version of Moq, v4.15 added this functionality https://github.com/moq/moq4/issues/918#issuecomment-720090418 – Gopal Krishnan Jan 18 '21 at 14:14
  • 1
    For those with Moq < 4.15.0, you can use this. It also doenst use `It.IsAnyType` which is not present in previous versions: `_loggerMock.Verify(x => x.Log(LogLevel.Information, It.IsAny(), It.Is(o => string.Equals($"Some log message", o.ToString(), StringComparison.InvariantCultureIgnoreCase)), It.IsAny(), (Func)It.IsAny()), Times.Once);` – Nick De Beer Oct 20 '21 at 15:53
  • This is great, but how did you figure out the proper format of the TState type? I couldn't find anything helpful anywhere. – Zach Jun 24 '22 at 20:34
  • @Zach Maybe I don't get your question, but you don't need to know the type of TState when you mock a logger since Moq supports `It.IsAnyType`. In the mock setup code there's nothing about type or shape of TState – Ivan Samygin Jun 26 '22 at 07:26
57

Use a custom logger that uses ITestOutputHelper (from xunit) to capture output and logs. The following is a small sample that only writes the state to the output.

public class XunitLogger<T> : ILogger<T>, IDisposable
{
    private ITestOutputHelper _output;

    public XunitLogger(ITestOutputHelper output)
    {
        _output = output;
    }
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        _output.WriteLine(state.ToString());
    }

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

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

    public void Dispose()
    {
    }
}

Use it in your unittests like

public class BlogControllerTest
{
  private XunitLogger<BlogController> _logger;

  public BlogControllerTest(ITestOutputHelper output){
    _logger = new XunitLogger<BlogController>(output);
  }

  [Fact]
  public void Index_ReturnAViewResult_WithAListOfBlog()
  {
    var mockRepo = new Mock<IDAO<Blog>>();
    mockRepo.Setup(repo => repo.GetMany(null)).Returns(GetListBlog());
    var controller = new BlogController(_logger,mockRepo.Object);
    // rest
  }
}
Jehof
  • 34,674
  • 10
  • 123
  • 155
  • 2
    hi. this work fine for me. now how i can check or view my log information – malik saifullah Dec 08 '18 at 13:04
  • i am running the unit test cases directly from VS. i don't have console for that – malik saifullah Dec 08 '18 at 15:34
  • can you please tell me how can i check log information i am running test cases from VS thanks – malik saifullah Dec 12 '18 at 05:32
  • 1
    @maliksaifullah im using resharper. let me check that with vs – Jehof Dec 12 '18 at 09:54
  • 1
    @maliksaifullah the TestExplorer of VS provides a link to open the output of a test. select your test in TestExplorer and on the bottom there is a link – Jehof Dec 12 '18 at 10:00
  • 4
    This is great, thanks! A couple suggestions: 1) this doesn't need to be generic, as the type parameter is not used. Implementing just `ILogger` will make it more broadly usable. 2) The `BeginScope` should not return itself, since that means any tested methods which begin and end a scope during the run will dispose the logger. Instead, create a private "dummy" nested class which implements `IDisposable` and return an instance of that (then remove `IDisposable` from `XunitLogger`). – Tobias J Feb 21 '20 at 19:55
  • @Jehof should it not be: `public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => _output.WriteLine(formatter(state, exception));` ? – Arturo Hernandez Jun 27 '22 at 16:45
26

Adding my 2 cents, This is a helper extension method typically put in a static helper class:

static class MockHelper
{
    public static ISetup<ILogger<T>> MockLog<T>(this Mock<ILogger<T>> logger, LogLevel level)
    {
        return logger.Setup(x => x.Log(level, It.IsAny<EventId>(), It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>()));
    }

    private static Expression<Action<ILogger<T>>> Verify<T>(LogLevel level)
    {
        return x => x.Log(level, 0, It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>());
    }

    public static void Verify<T>(this Mock<ILogger<T>> mock, LogLevel level, Times times)
    {
        mock.Verify(Verify<T>(level), times);
    }
}

Then, you use it like this:

//Arrange
var logger = new Mock<ILogger<YourClass>>();
logger.MockLog(LogLevel.Warning)

//Act

//Assert
logger.Verify(LogLevel.Warning, Times.Once());

And of course you can easily extend it to mock any expectation (i.e. expection, message, etc …)

Update for .NET 6 with Moq 4.17.2 This extension method allows also verifies the message using regex

static class MockHelper
{
    public static void VerifyLog<T>(this Mock<ILogger<T>> logger, LogLevel level, Times times, string? regex = null) =>
        logger.Verify(m => m.Log(
        level,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((x, y) => regex == null || Regex.IsMatch(x.ToString(), regex)),
        It.IsAny<Exception?>(),
        It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
        times);
}

And this is how to use it

logger.VerifyLog(LogLevel.Warning, Times.Exactly(2), "Simple match");
logger.VerifyLog(LogLevel.Warning, Times.Exactly(2), "[Yy]ou\scould do regex too.*");
Mahmoud Hanafy
  • 1,103
  • 12
  • 12
  • This is a very elegant solution. – MichaelDotKnox Jul 04 '19 at 14:27
  • 1
    I agree, this answer was very good. I don't understand why it doesn't have that many votes – Farzad Oct 25 '19 at 16:29
  • 1
    Fab. Here's a version for the non-generic `ILogger`: https://gist.github.com/timabell/d71ae82c6f3eaa5df26b147f9d3842eb – Tim Abell Jan 06 '20 at 14:25
  • 1
    Would it be possible to create mock to check the string we passed in LogWarning? For example: `It.Is(s => s.Equals("A parameter is empty!"))` – Serhat Jan 09 '20 at 08:35
  • This helps a lot. The one missing piece for me is how can I setup a callback on the mock that writes to XUnit output? Never hits the callback for me. – flipdoubt Mar 22 '20 at 13:50
  • I have an error: `The non-generic type 'ISetup' cannot be used with type arguments Wdp.Ecm.InternalApi.Testing` – Emanuele Mar 21 '22 at 16:49
  • Revisited this lately; here's one that works with .Net 6, and allows verifying the message; will update the answer. – Mahmoud Hanafy May 11 '22 at 07:50
21

The most easy solution is to use the NullLogger. It is part of Microsoft.Extensions.Logging.Abstractions.

No need to mess with factories and other unnecessary constructions. Just add:

ILogger<BlogController> logger = new NullLogger<BlogController>();
Ton Snoei
  • 2,637
  • 22
  • 23
10

If a still actual. Simple way do log to output in tests for .net core >= 3

[Fact]
public void SomeTest()
{
    using var logFactory = LoggerFactory.Create(builder => builder.AddConsole());
    var logger = logFactory.CreateLogger<AccountController>();
    
    var controller = new SomeController(logger);

    var result = controller.SomeActionAsync(new Dto{ ... }).GetAwaiter().GetResult();
}
sergeyxzc
  • 515
  • 6
  • 4
  • This was useful; While it doesn't MOCK the function it allows a test to continue without having a MOCK'ed logger. – FlyingV Nov 17 '20 at 04:58
  • Another benefit of this approach is that if you set minimum log level to debug, the tests will also run any code blocks guarded by logger.IsEnabled(..), potentially catching more bugs. – Egil Jan 21 '22 at 07:29
9

It is easy as other answers suggest to pass mock ILogger, but it suddenly becomes much more problematic to verify that calls actually were made to logger. The reason is that most calls do not actually belong to the ILogger interface itself.

So the most calls are extension methods that call the only Log method of the interface. The reason it seems is that it's way easier to make implementation of the interface if you have just one and not many overloads that boils down to same method.

The drawback is of course that it is suddenly much harder to verify that a call has been made since the call you should verify is very different from the call that you made. There are some different approaches to work around this, and I have found that custom extension methods for mocking framework will make it easiest to write.

Here is an example of a method that I have made to work with NSubstitute:

public static class LoggerTestingExtensions
{
    public static void LogError(this ILogger logger, string message)
    {
        logger.Log(
            LogLevel.Error,
            0,
            Arg.Is<FormattedLogValues>(v => v.ToString() == message),
            Arg.Any<Exception>(),
            Arg.Any<Func<object, Exception, string>>());
    }

}

And this is how it can be used:

_logger.Received(1).LogError("Something bad happened");   

It looks exactly as if you used the method directly, the trick here is that our extension method gets priority because it's "closer" in namespaces than the original one, so it will be used instead.

It does not give unfortunately 100% what we want, namely error messages will not be as good, since we don't check directly on a string but rather on a lambda that involves the string, but 95% is better than nothing :) Additionally this approach will make the test code

P.S. For Moq one can use the approach of writing an extension method for the Mock<ILogger<T>> that does Verify to achieve similar results.

P.P.S. This does not work in .Net Core 3 anymore, check this thread for more details: https://github.com/nsubstitute/NSubstitute/issues/597#issuecomment-573742574

Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207
  • Why do you verify logger calls? They are not a part of business logic. If something bad happened I'd rather verify the actual program behavior (such as calling an error handler or throwing an exception) than a logging a message. – Ilya Chumakov Mar 07 '19 at 17:19
  • 2
    Well I think it's quite important to test that as well, at least in some cases. I have seen too many times that a program fails silently, so I think it makes sense to verify that logging happened when an exception occurred e.g. And it's not like "either or", but rather testing both actual program behavior and logging. – Ilya Chernomordik Mar 08 '19 at 12:40
  • 'FormattedLogValues' is inaccessible due to its protection level. Cannot access internal struct 'FormattedLogValues' here. – Cristian E. Feb 08 '22 at 15:56
  • Please check the last P.P.S. – Ilya Chernomordik Feb 09 '22 at 11:07
9

Building even further on the work of @ivan-samygin and @stakx, here are extension methods that can also match on the Exception and all log values (KeyValuePairs).

These work (on my machine ;)) with .Net Core 3, Moq 4.13.0 and Microsoft.Extensions.Logging.Abstractions 3.1.0.

/// <summary>
/// Verifies that a Log call has been made, with the given LogLevel, Message and optional KeyValuePairs.
/// </summary>
/// <typeparam name="T">Type of the class for the logger.</typeparam>
/// <param name="loggerMock">The mocked logger class.</param>
/// <param name="expectedLogLevel">The LogLevel to verify.</param>
/// <param name="expectedMessage">The Message to verify.</param>
/// <param name="expectedValues">Zero or more KeyValuePairs to verify.</param>
public static void VerifyLog<T>(this Mock<ILogger<T>> loggerMock, LogLevel expectedLogLevel, string expectedMessage, params KeyValuePair<string, object>[] expectedValues)
{
    loggerMock.Verify(mock => mock.Log(
        expectedLogLevel,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((o, t) => MatchesLogValues(o, expectedMessage, expectedValues)),
        It.IsAny<Exception>(),
        It.IsAny<Func<object, Exception, string>>()
        )
    );
}

/// <summary>
/// Verifies that a Log call has been made, with LogLevel.Error, Message, given Exception and optional KeyValuePairs.
/// </summary>
/// <typeparam name="T">Type of the class for the logger.</typeparam>
/// <param name="loggerMock">The mocked logger class.</param>
/// <param name="expectedMessage">The Message to verify.</param>
/// <param name="expectedException">The Exception to verify.</param>
/// <param name="expectedValues">Zero or more KeyValuePairs to verify.</param>
public static void VerifyLog<T>(this Mock<ILogger<T>> loggerMock, string expectedMessage, Exception expectedException, params KeyValuePair<string, object>[] expectedValues)
{
    loggerMock.Verify(logger => logger.Log(
        LogLevel.Error,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((o, t) => MatchesLogValues(o, expectedMessage, expectedValues)),
        It.Is<Exception>(e => e == expectedException),
        It.Is<Func<It.IsAnyType, Exception, string>>((o, t) => true)
    ));
}

private static bool MatchesLogValues(object state, string expectedMessage, params KeyValuePair<string, object>[] expectedValues)
{
    const string messageKeyName = "{OriginalFormat}";

    var loggedValues = (IReadOnlyList<KeyValuePair<string, object>>)state;

    return loggedValues.Any(loggedValue => loggedValue.Key == messageKeyName && loggedValue.Value.ToString() == expectedMessage) &&
           expectedValues.All(expectedValue => loggedValues.Any(loggedValue => loggedValue.Key == expectedValue.Key && loggedValue.Value == expectedValue.Value));
}
Mathias R
  • 134
  • 1
  • 4
6

Already mentioned you can mock it as any other interface.

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

So far so good.

Nice thing is that you can use Moq to verify that certain calls have been performed. For instance here I check that the log has been called with a particular Exception.

logger.Verify(m => m.Log(It.Is<LogLevel>(l => l == LogLevel.Information), 0,
            It.IsAny<object>(), It.IsAny<TaskCanceledException>(), It.IsAny<Func<object, Exception, string>>()));

When using Verify the point is to do it against the real Log method from the ILooger interface and not the extension methods.

guillem
  • 2,768
  • 2
  • 30
  • 44
3

And when using StructureMap / Lamar:

var c = new Container(_ =>
{
    _.For(typeof(ILogger<>)).Use(typeof(NullLogger<>));
});

Docs:

Christoph Lütjen
  • 5,403
  • 2
  • 24
  • 33
3

Merely creating a dummy ILogger is not very valuable for unit testing. You should also verify that the logging calls were made. You can inject a mock ILogger with Moq but verifying the call can be a little tricky. This article goes into depth about verifying with Moq.

Here is a very simple example from the article:

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

It verifies that an information message was logged. But, if we want to verify more complex information about the message like the message template and the named properties, it gets more tricky:

_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<NotImplementedException>(),
    //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)
);

I'm sure that you could do the same with other mocking frameworks, but the ILogger interface ensures that it's difficult.

Christian Findlay
  • 6,770
  • 5
  • 51
  • 103
  • 1
    I agree with the sentiment, and as you say it can get a bit difficult building the expression. I had the same problem, often, so recently put together Moq.Contrib.ExpressionBuilders.Logging to provide a fluent interface which makes it a lot more palatable. – rgvlee Jul 08 '20 at 02:30
3

I have created a package, Moq.ILogger, to make testing ILogger extensions much easier.

You can actually use something like the following which is more close to your actual code.

loggerMock.VerifyLog(c => c.LogInformation(
                 "Index page say hello", 
                 It.IsAny<object[]>());

Not only it is easier to write new tests, but also the maintenance is with no costs.

The repo can be found here and there is a nuget package too (Install-Package ILogger.Moq).

I explained it also with a real-life example on my blog.

In short, let's say if you have the following code:

public class PaymentsProcessor
{
    private readonly IOrdersRepository _ordersRepository;
    private readonly IPaymentService _paymentService;
    private readonly ILogger<PaymentsProcessor> _logger;

    public PaymentsProcessor(IOrdersRepository ordersRepository, 
        IPaymentService paymentService, 
        ILogger<PaymentsProcessor> logger)
    {
        _ordersRepository = ordersRepository;
        _paymentService = paymentService;
        _logger = logger;
    }

    public async Task ProcessOutstandingOrders()
    {
        var outstandingOrders = await _ordersRepository.GetOutstandingOrders();
        
        foreach (var order in outstandingOrders)
        {
            try
            {
                var paymentTransaction = await _paymentService.CompletePayment(order);
                _logger.LogInformation("Order with {orderReference} was paid {at} by {customerEmail}, having {transactionId}", 
                                       order.OrderReference, 
                                       paymentTransaction.CreateOn, 
                                       order.CustomerEmail, 
                                       paymentTransaction.TransactionId);
            }
            catch (Exception e)
            {
                _logger.LogWarning(e, "An exception occurred while completing the payment for {orderReference}", 
                                   order.OrderReference);
            }
        }
        _logger.LogInformation("A batch of {0} outstanding orders was completed", outstandingOrders.Count);
    }
}

You could then write some tests like

[Fact]
public async Task Processing_outstanding_orders_logs_batch_size()
{
    // Arrange
    var ordersRepositoryMock = new Mock<IOrdersRepository>();
    ordersRepositoryMock.Setup(c => c.GetOutstandingOrders())
        .ReturnsAsync(GenerateOutstandingOrders(100));

    var paymentServiceMock = new Mock<IPaymentService>();
    paymentServiceMock
        .Setup(c => c.CompletePayment(It.IsAny<Order>()))
        .ReturnsAsync((Order order) => new PaymentTransaction
        {
            TransactionId = $"TRX-{order.OrderReference}"
        });

    var loggerMock = new Mock<ILogger<PaymentsProcessor>>();

    var sut = new PaymentsProcessor(ordersRepositoryMock.Object, paymentServiceMock.Object, loggerMock.Object);

    // Act
    await sut.ProcessOutstandingOrders();

    // Assert
    loggerMock.VerifyLog(c => c.LogInformation("A batch of {0} outstanding orders was completed", 100));
}

[Fact]
public async Task Processing_outstanding_orders_logs_order_and_transaction_data_for_each_completed_payment()
{
    // Arrange
    var ordersRepositoryMock = new Mock<IOrdersRepository>();
    ordersRepositoryMock.Setup(c => c.GetOutstandingOrders())
        .ReturnsAsync(GenerateOutstandingOrders(100));

    var paymentServiceMock = new Mock<IPaymentService>();
    paymentServiceMock
        .Setup(c => c.CompletePayment(It.IsAny<Order>()))
        .ReturnsAsync((Order order) => new PaymentTransaction
        {
            TransactionId = $"TRX-{order.OrderReference}"
        });

    var loggerMock = new Mock<ILogger<PaymentsProcessor>>();

    var sut = new PaymentsProcessor(ordersRepositoryMock.Object, paymentServiceMock.Object, loggerMock.Object);

    // Act
    await sut.ProcessOutstandingOrders();

    // Assert
    loggerMock.VerifyLog(logger => logger.LogInformation("Order with {orderReference} was paid {at} by {customerEmail}, having {transactionId}",
        It.Is<string>(orderReference => orderReference.StartsWith("Reference")),
        It.IsAny<DateTime>(),
        It.Is<string>(customerEmail => customerEmail.Contains("@")),
        It.Is<string>(transactionId => transactionId.StartsWith("TRX"))),
      Times.Exactly(100));
}

[Fact]
public async Task Processing_outstanding_orders_logs_a_warning_when_payment_fails()
{
    // Arrange
    var ordersRepositoryMock = new Mock<IOrdersRepository>();
    ordersRepositoryMock.Setup(c => c.GetOutstandingOrders())
        .ReturnsAsync(GenerateOutstandingOrders(2));

    var paymentServiceMock = new Mock<IPaymentService>();
    paymentServiceMock
        .SetupSequence(c => c.CompletePayment(It.IsAny<Order>()))
        .ReturnsAsync(new PaymentTransaction
        {
            TransactionId = "TRX-1",
            CreateOn = DateTime.Now.AddMinutes(-new Random().Next(100)),
        })
        .Throws(new Exception("Payment exception"));

    var loggerMock = new Mock<ILogger<PaymentsProcessor>>();

    var sut = new PaymentsProcessor(ordersRepositoryMock.Object, paymentServiceMock.Object, loggerMock.Object);

    // Act
    await sut.ProcessOutstandingOrders();

    // Assert
    loggerMock.VerifyLog(c => c.LogWarning(
                 It.Is<Exception>(paymentException => paymentException.Message.Contains("Payment exception")), 
                 "*exception*Reference 2"));
}
Adrian Iftode
  • 15,465
  • 4
  • 48
  • 73
2

I've tried to mock that Logger interface using NSubstitute (and failed because Arg.Any<T>() requeres a type parameter, which I can't provide), but ended up creating a test logger (similarly to @jehof's answer) in the following way:

    internal sealed class TestLogger<T> : ILogger<T>, IDisposable
    {
        private readonly List<LoggedMessage> _messages = new List<LoggedMessage>();

        public IReadOnlyList<LoggedMessage> Messages => _messages;

        public void Dispose()
        {
        }

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

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

        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
        {
            var message = formatter(state, exception);
            _messages.Add(new LoggedMessage(logLevel, eventId, exception, message));
        }

        public sealed class LoggedMessage
        {
            public LogLevel LogLevel { get; }
            public EventId EventId { get; }
            public Exception Exception { get; }
            public string Message { get; }

            public LoggedMessage(LogLevel logLevel, EventId eventId, Exception exception, string message)
            {
                LogLevel = logLevel;
                EventId = eventId;
                Exception = exception;
                Message = message;
            }
        }
    }

You can easily access all logged messages and assert all meaningful parameters provided with it.

Igor Kustov
  • 3,228
  • 2
  • 34
  • 31
0

@Mahmoud Hanafy

I updated your answer to work with the current state.

static class MockLogHelper
{
    public static ISetup<ILogger<T>> MockLog<T>(this Mock<ILogger<T>> logger, LogLevel level)
    {
        return logger.Setup(x => x.Log(level, It.IsAny<EventId>(), It.IsAny<It.IsAnyType>(), It.IsAny<Exception>(), (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));
        //return logger.Setup(x => x.Log(level, It.IsAny<EventId>(), It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>()));
    }

    private static Expression<Action<ILogger<T>>> Verify<T>(LogLevel level)
    {
        return x => x.Log(level, 0, It.IsAny<It.IsAnyType>(), It.IsAny<Exception>(), (Func<It.IsAnyType, Exception, string>)It.IsAny<object>());
        //return x => x.Log(level, 0, It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>());
    }

    public static void Verify<T>(this Mock<ILogger<T>> mock, LogLevel level, Times times)
    {
        mock.Verify(Verify<T>(level), times);
    }
}
Master Silver
  • 99
  • 1
  • 6
-1

Use Telerik Just Mock to create a mocked instance of the logger:

using Telerik.JustMock;
...
context = new XDbContext(Mock.Create<ILogger<XDbContext>>());
0x777
  • 825
  • 8
  • 14
-1

Use NullLogger - Minimalistic logger that does nothing.

public interface ILoggingClass
{
   public void LogCritical(Exception exception);
}

public class LoggingClass : ILoggingClass
{
    private readonly ILogger<LoggingClass> logger;

    public LoggingClass(ILogger<LoggingClass> logger) =>
            this.logger = logger;

    public void LogCritical(Exception exception) =>
        this.logger.LogCritical(exception, exception.Message);
}

and in the test method use,

ILogger<LoggingClass> logger = new NullLogger<LoggingClass>();
LoggingClass loggingClass = new LoggingClass(logger);

and pass the loggingClass to the service to test.

Jonathan Van Dam
  • 630
  • 9
  • 23