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());
}
}
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.
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>();
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);
}
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);
(Func<It.IsAnyType, Exception, string>) It.IsAny<object>())
with It.IsAny<Func<It.IsAnyType, Exception, string>>())
, but it doesnt work. What is this magic :-)?
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);
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
}
}
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
).
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.*");
ILogger
: gist.github.com/timabell/d71ae82c6f3eaa5df26b147f9d3842eb
It.Is<string>(s => s.Equals("A parameter is empty!"))
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();
}
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));
}
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.
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
And when using StructureMap / Lamar:
var c = new Container(_ =>
{
_.For(typeof(ILogger<>)).Use(typeof(NullLogger<>));
});
Docs:
https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.abstractions.nulllogger?view=aspnetcore-2.1
http://structuremap.github.io/generics/
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.
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.
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"));
}
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>();
@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);
}
}
Use Telerik Just Mock to create a mocked instance of the logger:
using Telerik.JustMock;
...
context = new XDbContext(Mock.Create<ILogger<XDbContext>>());
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.
Success story sharing
AddDebug
should be called inAddLogging
instead fromILoggerFactory
.new ServiceCollection().AddLogging(builder => builder.AddDebug())...