Although you can inject your ILogger<T>
into the constructor of your function, sometimes you may want to use the FunctionContext
from your trigger method.
Mocking this for testing can be a bit tricky.
Consider the following function:
public class MyFunction
{
[Function("MyFunction")]
public async Task Run([ActivityTrigger] FunctionContext executionContext)
{
var logger = executionContext
.GetLogger<MyFunction>();
logger.LogInformation("Hello");
await Task.CompletedTask;
}
}
We want to write tests for our class.
For this to be testable, we’ll need to somehow mock .GetLogger<MyFunction>
This method is actually an extension method – so can’t directly be mocked.
So we actually need to mock context.InstanceServices.GetService<T>
Here’s a full example of how we can achieve this:
public class MyTests
{
private readonly Mock<ILogger<MyFunction>> _mockLogger;
private readonly Mock<FunctionContext> _mockFunctionContext;
private readonly MyFunction _function;
public MyTests()
{
_mockLogger = new Mock<ILogger<MyFunction>>();
var services = new Mock<IServiceProvider>();
// Set up generic ILogger<T> mocking for any requested type
services
.Setup(x => x.GetService(typeof(ILogger<MyFunction>)))
.Returns(_mockLogger.Object);
_mockFunctionContext = new Mock<FunctionContext>();
_mockFunctionContext
.SetupGet(x => x.InstanceServices)
.Returns(services.Object);
_function = new MyFunction();
}
[Fact]
public async Task Logs_information_when_running()
{
await _function.Run(_mockFunctionContext.Object);
_mockLogger.VerifyLog(l => l.LogInformation("Hello"), Times.Once);
}
}
Reusing with a Test Fixture
Xunit has a fantastic feature for sharing context amongst tests called Class Fixtures https://xunit.net/docs/shared-context
We can leverage this to provide reusable mocked logger resolution.
You could have a Fixture<T>
where T is the type of logger you ultimately want to resolve, but I think this looks messy when injected.
You would have to do something like:
public class MyTests : IClassFixture<MyTestFixture<MyFunction>>
{
public MyTests(MyTestFixture<MyFunction> fixture)
{
// etc..
}
When what I’d really like to do is a more simple:
public class MyTests : IClassFixture<MyTestFixture>
{
private readonly MyTestFixture _fixture;
private readonly MyFunction _function;
public MyTests(MyTestFixture fixture)
{
_fixture = fixture;
_function = new MyFunction();
}
[Fact]
public async Task Logs_information_when_running()
{
await _function.Run(_fixture.MockFunctionContext.Object);
var logger = _fixture.GetLogger<MyFunction>();
logger.VerifyLog(l => l.LogInformation("Hello"), Times.Once);
}
}
We can achieve this by using a few little tricks in our test fixture:
public class MyTestFixture
{
private readonly Dictionary<Type, (Mock Mock, object Object)> _loggers = new();
public Mock<FunctionContext> MockFunctionContext { get; } = new();
public MyTestFixture()
{
var services = new Mock<IServiceProvider>();
services
.Setup(x => x.GetService(It.Is<Type>(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ILogger<>))))
.Returns
(
(Type serviceType) =>
{
var genericType = serviceType.GetGenericArguments()[0];
if (_loggers.TryGetValue(genericType, out var logger))
return logger.Object;
var mockLoggerType = typeof(Mock<>)
.MakeGenericType(typeof(ILogger<>)
.MakeGenericType(genericType));
var mockLogger = Activator.CreateInstance(mockLoggerType);
var mock = (Mock)mockLogger!;
_loggers[genericType] = (mock, mock.Object);
return _loggers[genericType].Object;
}
);
MockFunctionContext
.SetupGet(x => x.InstanceServices)
.Returns(services.Object);
}
public Mock<ILogger<T>> GetLogger<T>()
{
if (!_loggers.ContainsKey(typeof(T))) throw new Exception("Logger not found");
return (Mock<ILogger<T>>)_loggers[typeof(T)].Mock;
}
}
This way, the fixture supports mocking loggers for multiple types dynamically by resolving the correct logger based on the type passed to GetLogger<T>
.
This is achieved by storing the mock loggers in a dictionary and using IServiceProvider to resolve them based on the requested type. This makes it flexible and scalable for any function that uses a logger.
Leave a Reply