How to unit test with ILogger in ASP.NET Core
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
- https://stackoverflow.com/a/54646657/2164198
- https://stackoverflow.com/a/54809607/2164198
- https://stackoverflow.com/a/56728528/2164198
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.*");
Related videos on Youtube
duc
Updated on May 11, 2022Comments
-
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 aILogger
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 theILogger
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 almost 7 yearsYou 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 about 5 yearsAs @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 almost 7 yearsto log to the debug output window call AddDebug() on the factory: var factory = serviceProvider.GetService<ILoggerFactory>().AddDebug();
-
Thorkil Værge over 6 yearsThis seems to work for only .NET Core 2.0, not .NET Core 1.1.
-
DanielV almost 6 yearsI found the "real logger" approach more effective!
-
johnny 5 almost 6 years@adospace, your comment is way more useful than the answer
-
Martin Lottering over 5 yearsThe real logger part also works great for testing the LogConfiguration and LogLevel in specific scenarios.
-
malik saifullah over 5 yearshi. this work fine for me. now how i can check or view my log information
-
malik saifullah over 5 yearsi am running the unit test cases directly from VS. i don't have console for that
-
malik saifullah over 5 yearscan you please tell me how can i check log information i am running test cases from VS thanks
-
Jehof over 5 years@maliksaifullah im using resharper. let me check that with vs
-
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 about 5 yearsThis 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 about 5 yearsWhy 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 about 5 yearsWell 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 about 5 yearsCan 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 almost 5 yearsThis is a very elegant solution.
-
torylor almost 5 years@adospace Is this supposed to go in startup.cs?
-
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 over 4 yearsI agree, this answer was very good. I don't understand why it doesn't have that many votes
-
Matt over 4 yearsFab. Here's a version for the non-generic
ILogger
: gist.github.com/timabell/d71ae82c6f3eaa5df26b147f9d3842eb -
Serhat over 4 yearsWould 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 about 4 yearsThis 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) TheBeginScope
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 implementsIDisposable
and return an instance of that (then removeIDisposable
fromXunitLogger
). -
flipdoubt about 4 yearsThis 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 about 4 yearsYou saved my day..Thank you.
-
rgvlee almost 4 yearsI 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 over 3 yearsThis was useful; While it doesn't MOCK the function it allows a test to continue without having a MOCK'ed logger.
-
DotNetDev over 3 yearsIn fact, a very dumb and not helpful comment, the topic is about getting Moq to work, not using some commercial frameworks
-
Gopal Krishnan over 3 yearsI expected to be able to replace
(Func<It.IsAnyType, Exception, string>) It.IsAny<object>())
withIt.IsAny<Func<It.IsAnyType, Exception, string>>())
, but it doesnt work. What is this magic :-)? -
Gopal Krishnan over 3 yearsLoooks 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 about 3 yearsSince .net 5,
AddDebug
should be called inAddLogging
instead fromILoggerFactory
.new ServiceCollection().AddLogging(builder => builder.AddDebug())...
-
SerG about 3 yearsYou've missed Dispose calls.
-
qqtf almost 3 yearsthere'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-e8c16c503a80
-
Nick De Beer over 2 yearsFor 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 over 2 yearsBig thanks, You answer saved my time.
-
Brahmakumar M over 2 yearsAnother 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. over 2 years'FormattedLogValues' is inaccessible due to its protection level. Cannot access internal struct 'FormattedLogValues' here.
-
Ilya Chernomordik over 2 yearsPlease check the last P.P.S.
-
Emanuele about 2 yearsI have an error:
The non-generic type 'ISetup' cannot be used with type arguments Wdp.Ecm.InternalApi.Testing
-
Jack Miller about 2 yearsFor
AddDebug
you'll need to include nuget.org/packages/Microsoft.Extensions.Logging.Debug -
Mahmoud Hanafy about 2 yearsRevisited this lately; here's one that works with .Net 6, and allows verifying the message; will update the answer.