Durable Functions unit testing (C# Isolated)

Unit testing is an important part of modern software development practices. Unit tests verify business logic behavior and protect from introducing unnoticed breaking changes in the future. Durable Functions can easily grow in complexity so introducing unit tests helps avoid breaking changes. The following sections explain how to unit test the three function types - Orchestration client, orchestrator, and activity functions.

Note

Prerequisites

The examples in this article require knowledge of the following concepts and frameworks:

  • Unit testing
  • Durable Functions
  • xUnit - Testing framework
  • moq - Mocking framework

Base classes for mocking

Mocking is supported via the following interfaces and classes:

  • DurableTaskClient - For orchestrator client operations
  • TaskOrchestrationContext - For orchestrator function execution
  • FunctionContext - For function execution context
  • HttpRequestData and HttpResponseData - For HTTP trigger functions

These classes can be used with the various trigger and bindings supported by Durable Functions. While it's executing your Azure Functions, the functions runtime runs your function code with concrete implementations of these classes. For unit testing, you can pass in a mocked version of these classes to test your business logic.

Unit testing trigger functions

In this section, the unit test validates the logic of the following HTTP trigger function for starting new orchestrations.

[Function("HelloCitiesOrchestration_HttpStart")]
public static async Task<HttpResponseData> HttpStart(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
    [DurableClient] DurableTaskClient client,
    FunctionContext executionContext)
{
    // Function input comes from the request content.
    string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
        nameof(HelloCitiesOrchestration));

    // Returns an HTTP 202 response with an instance management payload.
    return await client.CreateCheckStatusResponseAsync(req, instanceId);
}

The unit test verifies the HTTP response status code and the instance ID in the response payload. The test mocks the DurableTaskClient to ensure predictable behavior.

First, we use a mocking framework (Moq in this case) to mock DurableTaskClient and FunctionContext:

// Mock DurableTaskClient
var durableClientMock = new Mock<DurableTaskClient>("testClient");
var functionContextMock = new Mock<FunctionContext>();

Then ScheduleNewOrchestrationInstanceAsync method is mocked to return an instance ID:

var instanceId = Guid.NewGuid().ToString();

// Mock ScheduleNewOrchestrationInstanceAsync method
durableClientMock
    .Setup(x => x.ScheduleNewOrchestrationInstanceAsync(
        It.IsAny<TaskName>(),
        It.IsAny<object>(),
        It.IsAny<StartOrchestrationOptions>(),
        It.IsAny<CancellationToken>()))
    .ReturnsAsync(instanceId);

Next, we need to mock the HTTP request and response data:

// Mock HttpRequestData that sent to the http trigger
var mockRequest = MockHttpRequestAndResponseData();

var responseMock = new Mock<HttpResponseData>(functionContextMock.Object);
responseMock.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.Accepted);

Here's the complete helper method for mocking HttpRequestData:

private HttpRequestData MockHttpRequestAndResponseData(HttpHeadersCollection? headers = null)
{
    var mockObjectSerializer = new Mock<ObjectSerializer>();

    // Setup the SerializeAsync method
    mockObjectSerializer.Setup(s => s.SerializeAsync(It.IsAny<Stream>(), It.IsAny<object?>(), It.IsAny<Type>(), It.IsAny<CancellationToken>()))
        .Returns<Stream, object?, Type, CancellationToken>(async (stream, value, type, token) =>
        {
            await System.Text.Json.JsonSerializer.SerializeAsync(stream, value, type, cancellationToken: token);
        });

    var workerOptions = new WorkerOptions
    {
        Serializer = mockObjectSerializer.Object
    };
    var mockOptions = new Mock<IOptions<WorkerOptions>>();
    mockOptions.Setup(o => o.Value).Returns(workerOptions);

    // Mock the service provider
    var mockServiceProvider = new Mock<IServiceProvider>();

    // Set up the service provider to return the mock IOptions<WorkerOptions>
    mockServiceProvider.Setup(sp => sp.GetService(typeof(IOptions<WorkerOptions>)))
        .Returns(mockOptions.Object);

    // Set up the service provider to return the mock ObjectSerializer
    mockServiceProvider.Setup(sp => sp.GetService(typeof(ObjectSerializer)))
        .Returns(mockObjectSerializer.Object);

    // Create a mock FunctionContext and assign the service provider
    var mockFunctionContext = new Mock<FunctionContext>();
    mockFunctionContext.SetupGet(c => c.InstanceServices).Returns(mockServiceProvider.Object);
    var mockHttpRequestData = new Mock<HttpRequestData>(mockFunctionContext.Object);

    // Set up the URL property
    mockHttpRequestData.SetupGet(r => r.Url).Returns(new Uri("https://localhost:7075/orchestrators/HelloCities"));

    // If headers are provided, use them, otherwise create a new empty HttpHeadersCollection
    headers ??= new HttpHeadersCollection();

    // Setup the Headers property to return the empty headers
    mockHttpRequestData.SetupGet(r => r.Headers).Returns(headers);

    var mockHttpResponseData = new Mock<HttpResponseData>(mockFunctionContext.Object)
    {
        DefaultValue = DefaultValue.Mock
    };

    // Enable setting StatusCode and Body as mutable properties
    mockHttpResponseData.SetupProperty(r => r.StatusCode, HttpStatusCode.OK);
    mockHttpResponseData.SetupProperty(r => r.Body, new MemoryStream());

    // Setup CreateResponse to return the configured HttpResponseData mock
    mockHttpRequestData.Setup(r => r.CreateResponse())
        .Returns(mockHttpResponseData.Object);

    return mockHttpRequestData.Object;
}

Finally, we call the function and verify the results:

var result = await HelloCitiesOrchestration.HttpStart(mockRequest, durableClientMock.Object, functionContextMock.Object);

// Verify the status code
Assert.Equal(HttpStatusCode.Accepted, result.StatusCode);

// Reset stream position for reading
result.Body.Position = 0;
var serializedResponseBody = await System.Text.Json.JsonSerializer.DeserializeAsync<dynamic>(result.Body);

// Verify the response returned contains the right data
Assert.Equal(instanceId, serializedResponseBody!.GetProperty("Id").GetString());

Note

Currently, loggers created via FunctionContext in trigger functions aren't supported for mocking in unit tests.

Unit testing orchestrator functions

Orchestrator functions manage the execution of multiple activity functions. To test an orchestrator:

  • Mock the TaskOrchestrationContext to control function execution
  • Replace TaskOrchestrationContext methods needed for orchestrator execution like CallActivityAsync with mock functions
  • Call the orchestrator directly with the mocked context
  • Verify the orchestrator results using assertions

In this section, the unit test validates the behavior of the HelloCities orchestrator function:

[Function(nameof(HelloCitiesOrchestration))]
public static async Task<List<string>> HelloCities(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    ILogger logger = context.CreateReplaySafeLogger(nameof(HelloCities));
    logger.LogInformation("Saying hello.");
    var outputs = new List<string>();

    outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "Tokyo"));
    outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "Seattle"));
    outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "London"));

    return outputs;
}

The unit test verifies that the orchestrator calls the correct activity functions with the expected parameters and returns the expected results. The test mocks the TaskOrchestrationContext to ensure predictable behavior.

First, we use a mocking framework (Moq in this case) to mock TaskOrchestrationContext:

// Mock TaskOrchestrationContext and setup logger
var contextMock = new Mock<TaskOrchestrationContext>();

Then we mock the CreateReplaySafeLogger method to return our test logger:

testLogger = new Mock<ILogger>();
contextMock.Setup(x => x.CreateReplaySafeLogger(It.IsAny<string>()))
    .Returns(testLogger.Object);

Next, we mock the activity function calls with specific return values for each city:

// Mock the activity function calls
contextMock.Setup(x => x.CallActivityAsync<string>(
    It.Is<TaskName>(n => n.Name == nameof(HelloCitiesOrchestration.SayHello)),
    It.Is<string>(n => n == "Tokyo"),
    It.IsAny<TaskOptions>()))
    .ReturnsAsync("Hello Tokyo!");
contextMock.Setup(x => x.CallActivityAsync<string>(
    It.Is<TaskName>(n => n.Name == nameof(HelloCitiesOrchestration.SayHello)),
    It.Is<string>(n => n == "Seattle"),
    It.IsAny<TaskOptions>()))
    .ReturnsAsync("Hello Seattle!");
contextMock.Setup(x => x.CallActivityAsync<string>(
    It.Is<TaskName>(n => n.Name == nameof(HelloCitiesOrchestration.SayHello)),
    It.Is<string>(n => n == "London"),
    It.IsAny<TaskOptions>()))
    .ReturnsAsync("Hello London!");

Then we call the orchestrator function with the mocked context:

var result = await HelloCitiesOrchestration.HelloCities(contextMock.Object);

Finally, we verify the orchestration result and logging behavior:

// Verify the orchestration result
Assert.Equal(3, result.Count);
Assert.Equal("Hello Tokyo!", result[0]);
Assert.Equal("Hello Seattle!", result[1]);
Assert.Equal("Hello London!", result[2]);

// Verify logging
testLogger.Verify(
    x => x.Log(
        It.Is<LogLevel>(l => l == LogLevel.Information),
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Saying hello")),
        It.IsAny<Exception>(),
        It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
    Times.Once);

Unit testing activity functions

Activity functions require no Durable-specific modifications to be tested. The guidance found in the Azure Functions unit testing overview is sufficient for testing these functions.

In this section, the unit test validates the behavior of the SayHello Activity function:

[Function(nameof(SayHello))]
public static string SayHello([ActivityTrigger] string name, FunctionContext executionContext)
{
    return $"Hello {name}!";
}

The unit test verifies the format of the output:

[Fact]
public void SayHello_ReturnsExpectedGreeting()
{
    var functionContextMock = new Mock<FunctionContext>();
    
    const string name = "Tokyo";

    var result = HelloCitiesOrchestration.SayHello(name, functionContextMock.Object);

    // Verify the activity function SayHello returns the right result
    Assert.Equal($"Hello {name}!", result);
}

Note

Currently, loggers created via FunctionContext in activity functions aren't supported for mocking in unit tests.

Next steps