Mocking FunctionContext GetLogger using Durable Functions Isolated Worker

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.

GetLogger

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

Your email address will not be published. Required fields are marked *