How to unit test with ILogger in ASP.NET Core

145,281

Solution 1

Just mock it as well as any other dependency:

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

//or use this short equivalent 
logger = Mock.Of<ILogger<BlogController>>()

var controller = new BlogController(logger);

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

Solution 2

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

Solution 3

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

Solution 4

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

Solution 5

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.*");
Share:
145,281

Related videos on Youtube

duc
Author by

duc

Updated on May 11, 2022

Comments

  • duc
    duc about 2 years

    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());
        }
    }
    
    • ssmith
      ssmith almost 7 years
      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 showing a variety of approaches. The article includes a full GitHub repo with each of the different options. In the end, my recommendation is to use your own adapter rather than working directly with the ILogger<T> type, if you need to be able to
    • Ilya Chernomordik
      Ilya Chernomordik about 5 years
      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.
  • spottedmahn
    spottedmahn almost 7 years
    to log to the debug output window call AddDebug() on the factory: var factory = serviceProvider.GetService<ILoggerFactory>().AddDebug();
  • Thorkil Værge
    Thorkil Værge over 6 years
    This seems to work for only .NET Core 2.0, not .NET Core 1.1.
  • DanielV
    DanielV almost 6 years
    I found the "real logger" approach more effective!
  • johnny 5
    johnny 5 almost 6 years
    @adospace, your comment is way more useful than the answer
  • Martin Lottering
    Martin Lottering over 5 years
    The real logger part also works great for testing the LogConfiguration and LogLevel in specific scenarios.
  • malik saifullah
    malik saifullah over 5 years
    hi. this work fine for me. now how i can check or view my log information
  • malik saifullah
    malik saifullah over 5 years
    i am running the unit test cases directly from VS. i don't have console for that
  • malik saifullah
    malik saifullah over 5 years
    can you please tell me how can i check log information i am running test cases from VS thanks
  • Jehof
    Jehof over 5 years
    @maliksaifullah im using resharper. let me check that with vs
  • Jehof
    Jehof over 5 years
    @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
  • Ilya Chernomordik
    Ilya Chernomordik about 5 years
    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.
  • Ilya Chumakov
    Ilya Chumakov about 5 years
    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 Chernomordik
    Ilya Chernomordik about 5 years
    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.
  • J86
    J86 about 5 years
    Can you give an example of how this would work? When unit testing, I'd like logs to appear in the output window, I'm not sure if this does that.
  • MichaelDotKnox
    MichaelDotKnox almost 5 years
    This is a very elegant solution.
  • torylor
    torylor almost 5 years
    @adospace Is this supposed to go in startup.cs?
  • adospace
    adospace almost 5 years
    @raklos hum, no it's supposed to be used in a startup method inside the test where the ServiceCollection is instantiated
  • Farzad
    Farzad over 4 years
    I agree, this answer was very good. I don't understand why it doesn't have that many votes
  • Matt
    Matt over 4 years
    Fab. Here's a version for the non-generic ILogger: gist.github.com/timabell/d71ae82c6f3eaa5df26b147f9d3842eb
  • Serhat
    Serhat over 4 years
    Would it be possible to create mock to check the string we passed in LogWarning? For example: It.Is<string>(s => s.Equals("A parameter is empty!"))
  • Tobias J
    Tobias J about 4 years
    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).
  • flipdoubt
    flipdoubt about 4 years
    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.
  • KiddoDeveloper
    KiddoDeveloper about 4 years
    You saved my day..Thank you.
  • rgvlee
    rgvlee almost 4 years
    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.
  • FlyingV
    FlyingV over 3 years
    This was useful; While it doesn't MOCK the function it allows a test to continue without having a MOCK'ed logger.
  • DotNetDev
    DotNetDev over 3 years
    In fact, a very dumb and not helpful comment, the topic is about getting Moq to work, not using some commercial frameworks
  • Gopal Krishnan
    Gopal Krishnan over 3 years
    I expected to be able to replace (Func<It.IsAnyType, Exception, string>) It.IsAny<object>()) with It.IsAny<Func<It.IsAnyType, Exception, string>>()), but it doesnt work. What is this magic :-)?
  • Gopal Krishnan
    Gopal Krishnan over 3 years
    Loooks like I just need to update to latest version of Moq, v4.15 added this functionality github.com/moq/moq4/issues/918#issuecomment-720090418
  • Cyril Durand
    Cyril Durand about 3 years
    Since .net 5, AddDebug should be called in AddLogging instead from ILoggerFactory. new ServiceCollection().AddLogging(builder => builder.AddDebug())...
  • SerG
    SerG about 3 years
    You've missed Dispose calls.
  • qqtf
    qqtf almost 3 years
    there's an article on Unit Testing with ILogger, which briefly mentions the NullLogger-option as well, and adds a complete unit testing with Ilogger<T>-repository, for those who want to find detailed information: codeburst.io/unit-testing-with-net-core-ilogger-t-e8c16c503a‌​80
  • Nick De Beer
    Nick De Beer over 2 years
    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<EventId>(), It.Is<object>(o => string.Equals($"Some log message", o.ToString(), StringComparison.InvariantCultureIgnoreCase)), It.IsAny<Exception>(), (Func<object, Exception, string>)It.IsAny<object>()), Times.Once);
  • Roman Z
    Roman Z over 2 years
    Big thanks, You answer saved my time.
  • Brahmakumar M
    Brahmakumar M over 2 years
    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.
  • Cristian E.
    Cristian E. over 2 years
    'FormattedLogValues' is inaccessible due to its protection level. Cannot access internal struct 'FormattedLogValues' here.
  • Ilya Chernomordik
    Ilya Chernomordik over 2 years
    Please check the last P.P.S.
  • Emanuele
    Emanuele about 2 years
    I have an error: The non-generic type 'ISetup' cannot be used with type arguments Wdp.Ecm.InternalApi.Testing
  • Jack Miller
    Jack Miller about 2 years
    For AddDebug you'll need to include nuget.org/packages/Microsoft.Extensions.Logging.Debug
  • Mahmoud Hanafy
    Mahmoud Hanafy about 2 years
    Revisited this lately; here's one that works with .Net 6, and allows verifying the message; will update the answer.