Mocking FunctionContext GetLogger using Durable Functions Isolated Worker

Alex Brown
3 min read17 hours ago

--

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.

Originally published at https://www.alexjamesbrown.com on September 18, 2024.

--

--