I'm working on a fun project for myself to learn blazor, a lot of core concepts and just general practice.
I'm trying to implement logger extension for FakeItEasy to easier test Microsofts ILogger.
Based on https://dev.azure.com/BrideButler/Bride%20Butler%20Open%20Source/_git/FakeItEasy.DotnetCore.LoggerExtensions?path=%2FFakeItEasy.DotnetCore.LoggerExtensions%2FLoggerExtensions.cs but now trying to get it to work for .NET 5.
Now that FormattedLogValues is internal this code doesn't work anymore.
The goal is to have something like this in my test (and not use Moq)
_logger.VerifyLog(LogLevel.Information, "Get Weather Called").MustHaveHappenedOnceExactly();
or A.CallTo(() => _logger.Log(LogLevel.Information, "Get Weather Called").MustHaveHappenedOnceExactly(); It doesn't have to have syntactic sugar but I would like to get it to work with FakeItEasy instead of Moq.
Below is all the involved code. Ofcourse I see some difference in the first line Microsoft.Extensions.Logging.ILogger.Log1[System.Object]vsMicrosoft.Extensions.Logging.ILogger1[Server.Controllers.WeatherForecastController].Log1[Microsoft.Extensions.Logging.FormattedLogValues]` but I've no clue how to "fix" this since I dont fully understand what is going wrong.
My question is obviously, what should I do to fix it, but also curious what part i'm missing.
My error is the following:
Assertion failed for the following call:
Microsoft.Extensions.Logging.ILogger.Log`1[System.Object]
(
logLevel: Information,
eventId: <Ignored>,
state: <e => e.ToString().Contains(value(Server.Tests.Extensions.LoggerExtensions+<>c__DisplayClass2_0`1[Server.Controllers.WeatherForecastController]).message)>,
exception: <Ignored>,
formatter: <Ignored>
)
Expected to find it once exactly but didn't find it among the calls:
1: Microsoft.Extensions.Logging.ILogger`1[Server.Controllers.WeatherForecastController].Log`1[Microsoft.Extensions.Logging.FormattedLogValues]
(
logLevel: Information,
eventId: 0,
state: [[{OriginalFormat}, Get Weather Called]],
exception: NULL,
formatter: System.Func`3[Microsoft.Extensions.Logging.FormattedLogValues,System.Exception,System.String]
)
Tests:
(using xUnit, FakeItEasy, Fixture)
LoggerExtensions
public static class LoggerExtensions
{
public static void VerifyLogMustHaveHappened<T>(this ILogger<T> logger, LogLevel level, string message)
{
try
{
logger.VerifyLog(level, message)
.MustHaveHappened();
}
catch (Exception e)
{
throw new ExpectationException($"while verifying a call to log with message: \"{message}\"", e);
}
}
public static void VerifyLogMustNotHaveHappened<T>(this ILogger<T> logger, LogLevel level, string message)
{
try
{
logger.VerifyLog(level, message)
.MustNotHaveHappened();
}
catch (Exception e)
{
throw new ExpectationException($"while verifying a call to log with message: \"{message}\"", e);
}
}
public static IVoidArgumentValidationConfiguration VerifyLog<T>(this ILogger<T> logger, LogLevel level,
string message)
{
return A.CallTo(() => logger.Log(
level,
A<EventId>._,
A<object>.That.Matches(e => e.ToString().Contains(message)),
A<Exception>._,
A<Func<object, Exception, string>>._)
);
}
}
TestConstructor:
private readonly IFixture _fixture;
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastControllerTests()
{
_fixture = new Fixture().Customize(new AutoFakeItEasyCustomization());
_logger = _fixture.Freeze<Fake<ILogger<WeatherForecastController>>>().FakedObject;
}
Test:
[Fact]
public void WeatherForecast_Get_Should_Log()
{
// Arrange
var weatherController = new WeatherForecastController(_logger);
// Act
weatherController.Get();
// Assert
_logger.VerifyLog(LogLevel.Information, "Get Weather Called").MustHaveHappenedOnceExactly();
}
Controller:
public class WeatherForecastController : ControllerBase
{
private readonly ILogger _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
// Left out other code then logging
_logger.Log(LogLevel.Information, "Get Weather Called");
}
}
Update:
Options found
Only test if logger was called with correct log level:
public static IVoidArgumentValidationConfiguration VerifyLog<T>(this ILogger<T> logger, LogLevel level, string message)
{
return A.CallTo(logger).Where(call => call.Method.Name == "Log" && call.GetArgument<LogLevel>(0) == level);
}
Switch to Moq:
https://stackoverflow.com/a/59182490/1112413
https://stackoverflow.com/a/56728528/1112413
After reading https://stackoverflow.com/users/4728685/pavel-anikhouski comment on the post. I "got" what was wrong. Went to countless topics and made a solution that "works" tho probably could use some better coding standards. Hope it helps anyone else!
Edit: Some explaining.
Found this github comment that is the base of the solution. Then found this github pr where they mentioned to change Microsoft.Extensions.Logging.FormattedLogValues to IReadOnlyList<KeyValuePair<string, object>>
This can be extracted from GetArgument(2). (Source: https://stackoverflow.com/a/63701968/1112413)
public static class LoggerExtensions
{
public static void VerifyLogMustHaveHappened<T>(this ILogger<T> logger, LogLevel level, string message)
{
try
{
logger.VerifyLog(level, message)
.MustHaveHappened();
}
catch (Exception e)
{
throw new ExpectationException($"while verifying a call to log with message: \"{message}\"", e);
}
}
public static void VerifyLogMustNotHaveHappened<T>(this ILogger<T> logger, LogLevel level, string message)
{
try
{
logger.VerifyLog(level, message)
.MustNotHaveHappened();
}
catch (Exception e)
{
throw new ExpectationException($"while verifying a call to log with message: \"{message}\"", e);
}
}
public static IVoidArgumentValidationConfiguration VerifyLog<T>(this ILogger<T> logger, LogLevel level, string message)
{
return A.CallTo(logger)
.Where(call => call.Method.Name == "Log"
&& call.GetArgument<LogLevel>(0) == level
&& CheckLogMessages(call.GetArgument<IReadOnlyList<KeyValuePair<string, object>>>(2), message));
}
private static bool CheckLogMessages(IReadOnlyList<KeyValuePair<string, object>> readOnlyLists, string message)
{
foreach(var kvp in readOnlyLists)
{
if (kvp.Value.ToString().Contains(message))
{
return true;
}
}
return false;
}
}
Based on the same comments of Github
I ended up with a solution like that:
public class MyClassWithLoggerTests
{
private readonly MyClassWithLogger _sut;
private readonly ILogger<MyClassWithLogger> _logger;
public MyClassWithLoggerTests()
{
this._logger = A.Fake<ILogger<MyClassWithLogger>>();
this._sut = new MyClassWithLogger(this._logger);
}
[Fact]
public ATestMethodTitle()
{
this._sut.MethodToTest();
A.CallTo(_logger).Where(
call => call.Method.Name == "Log" && call.GetArgument<LogLevel>(0) == LogLevel.Information) // Modify with your type of log
.MustHaveHappened(1, Times.Exactly); // Modify with the number of times log is called
}
}
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());
}
}
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);
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
}
}
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.*");
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>();
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();
}
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));
}
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.
And when using StructureMap / Lamar:
var c = new Container(_ =>
{
_.For(typeof(ILogger<>)).Use(typeof(NullLogger<>));
});
Docs:
https://learn.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"));
}
#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.