This is my controller:
public class BlogController : Controller
{
private IDAO _blogDAO;
private readonly ILogger _
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>();
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"));
}
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.
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);
}
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
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));
}