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 will help to avoid breaking changes. The following sections explain how to unit test the three function types - Orchestration client, orchestrator, and activity functions.
Piezīme
This article provides guidance for unit testing for Durable Functions apps written in C# for the .NET in-process worker and targeting Durable Functions 2.x. For more information about the differences between versions, see the Durable Functions versions article.
Prerequisites
The examples in this article require knowledge of the following concepts and frameworks:
These interfaces can be used with the various trigger and bindings supported by Durable Functions. When executing your Azure Functions, the functions runtime will run your function code with a concrete implementation of these interfaces. For unit testing, you can pass in a mocked version of these interfaces to test your business logic.
Unit testing trigger functions
In this section, the unit test will validate the logic of the following HTTP trigger function for starting new orchestrations.
C#
// Copyright (c) .NET Foundation. All rights reserved.// Licensed under the MIT License. See LICENSE in the project root for license information.using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
namespaceVSSample
{
publicstaticclassHttpStart
{
[FunctionName("HttpStart")]
publicstaticasync Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Function, methods: "post", Route = "orchestrators/{functionName}")] HttpRequestMessage req,
[DurableClient] IDurableClient starter,
string functionName,
ILogger log)
{
// Function input comes from the request content.object eventData = await req.Content.ReadAsAsync<object>();
string instanceId = await starter.StartNewAsync(functionName, eventData);
log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
return starter.CreateCheckStatusResponse(req, instanceId);
}
}
}
The unit test task will be to verify the value of the Retry-After header provided in the response payload. So the unit test will mock some of IDurableClient methods to ensure predictable behavior.
First, we use a mocking framework (moq in this case) to mock IDurableClient:
C#
// Mock IDurableClientvar durableClientMock = new Mock<IDurableClient>();
Piezīme
While you can mock interfaces by directly implementing the interface as a class, mocking frameworks simplify the process in various ways. For instance, if a new method is added to the interface across minor releases, moq will not require any code changes unlike concrete implementations.
Then StartNewAsync method is mocked to return a well-known instance ID.
Next CreateCheckStatusResponse is mocked to always return an empty HTTP 200 response.
C#
// Mock CreateCheckStatusResponse method
durableClientMock
// Notice that even though the HttpStart function does not call IDurableClient.CreateCheckStatusResponse() // with the optional parameter returnInternalServerErrorOnFailure, moq requires the method to be set up// with each of the optional parameters provided. Simply use It.IsAny<> for each optional parameter
.Setup(x => x.CreateCheckStatusResponse(It.IsAny<HttpRequestMessage>(), instanceId, returnInternalServerErrorOnFailure: It.IsAny<bool>()))
.Returns(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(string.Empty),
Headers =
{
RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10))
}
});
ILogger is also mocked:
C#
// Mock ILoggervar loggerMock = new Mock<ILogger>();
Now the Run method is called from the unit test:
C#
// Call Orchestration trigger functionvar result = await HttpStart.Run(
new HttpRequestMessage()
{
Content = new StringContent("{}", Encoding.UTF8, "application/json"),
RequestUri = new Uri("http://localhost:7071/orchestrators/E1_HelloSequence"),
},
durableClientMock.Object,
functionName,
loggerMock.Object);
The last step is to compare the output with the expected value:
C#
// Validate that output is not null
Assert.NotNull(result.Headers.RetryAfter);
// Validate output's Retry-After header value
Assert.Equal(TimeSpan.FromSeconds(10), result.Headers.RetryAfter.Delta);
After combining all steps, the unit test will have the following code:
C#
// Copyright (c) .NET Foundation. All rights reserved.// Licensed under the MIT License. See LICENSE in the project root for license information.namespaceVSSample.Tests
{
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Net.Http.Headers;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
publicclassHttpStartTests
{
[Fact]
publicasync Task HttpStart_returns_retryafter_header()
{
// Define constantsconststring functionName = "SampleFunction";
conststring instanceId = "7E467BDB-213F-407A-B86A-1954053D3C24";
// Mock TraceWritervar loggerMock = new Mock<ILogger>();
// Mock DurableOrchestrationClientBasevar clientMock = new Mock<IDurableClient>();
// Mock StartNewAsync method
clientMock.
Setup(x => x.StartNewAsync(functionName, It.IsAny<string>(), It.IsAny<object>())).
ReturnsAsync(instanceId);
// Mock CreateCheckStatusResponse method
clientMock
.Setup(x => x.CreateCheckStatusResponse(It.IsAny<HttpRequestMessage>(), instanceId, false))
.Returns(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(string.Empty),
Headers =
{
RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10))
}
});
// Call Orchestration trigger functionvar result = await HttpStart.Run(
new HttpRequestMessage()
{
Content = new StringContent("{}", Encoding.UTF8, "application/json"),
RequestUri = new Uri("http://localhost:7071/orchestrators/E1_HelloSequence"),
},
clientMock.Object,
functionName,
loggerMock.Object);
// Validate that output is not null
Assert.NotNull(result.Headers.RetryAfter);
// Validate output's Retry-After header value
Assert.Equal(TimeSpan.FromSeconds(10), result.Headers.RetryAfter.Delta);
}
}
}
Unit testing orchestrator functions
Orchestrator functions are even more interesting for unit testing since they usually have a lot more business logic.
In this section the unit tests will validate the output of the E1_HelloSequence Orchestrator function:
C#
// Copyright (c) .NET Foundation. All rights reserved.// Licensed under the MIT License. See LICENSE in the project root for license information.using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
namespaceVSSample
{
publicstaticclassHelloSequence
{
[FunctionName("E1_HelloSequence")]
publicstaticasync Task<List<string>> Run(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var outputs = new List<string>();
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Tokyo"));
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Seattle"));
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello_DirectInput", "London"));
// returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]return outputs;
}
[FunctionName("E1_SayHello")]
publicstaticstringSayHello([ActivityTrigger] IDurableActivityContext context)
{
string name = context.GetInput<string>();
return$"Hello {name}!";
}
[FunctionName("E1_SayHello_DirectInput")]
publicstaticstringSayHelloDirectInput([ActivityTrigger] string name)
{
return$"Hello {name}!";
}
}
}
The unit test code will start with creating a mock:
C#
var durableOrchestrationContextMock = new Mock<IDurableOrchestrationContext>();
After combining all steps, the unit test will have the following code:
C#
// Copyright (c) .NET Foundation. All rights reserved.// Licensed under the MIT License. See LICENSE in the project root for license information.namespaceVSSample.Tests
{
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Moq;
using Xunit;
publicclassHelloSequenceTests
{
[Fact]
publicasync Task Run_returns_multiple_greetings()
{
var mockContext = new Mock<IDurableOrchestrationContext>();
mockContext.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Tokyo")).ReturnsAsync("Hello Tokyo!");
mockContext.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Seattle")).ReturnsAsync("Hello Seattle!");
mockContext.Setup(x => x.CallActivityAsync<string>("E1_SayHello_DirectInput", "London")).ReturnsAsync("Hello London!");
var result = await HelloSequence.Run(mockContext.Object);
Assert.Equal(3, result.Count);
Assert.Equal("Hello Tokyo!", result[0]);
Assert.Equal("Hello Seattle!", result[1]);
Assert.Equal("Hello London!", result[2]);
}
}
}
Unit testing activity functions
Activity functions can be unit tested in the same way as non-durable functions.
In this section the unit test will validate the behavior of the E1_SayHello Activity function:
C#
// Copyright (c) .NET Foundation. All rights reserved.// Licensed under the MIT License. See LICENSE in the project root for license information.using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
namespaceVSSample
{
publicstaticclassHelloSequence
{
[FunctionName("E1_HelloSequence")]
publicstaticasync Task<List<string>> Run(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var outputs = new List<string>();
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Tokyo"));
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Seattle"));
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello_DirectInput", "London"));
// returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]return outputs;
}
[FunctionName("E1_SayHello")]
publicstaticstringSayHello([ActivityTrigger] IDurableActivityContext context)
{
string name = context.GetInput<string>();
return$"Hello {name}!";
}
[FunctionName("E1_SayHello_DirectInput")]
publicstaticstringSayHelloDirectInput([ActivityTrigger] string name)
{
return$"Hello {name}!";
}
}
}
And the unit tests will verify the format of the output. The unit tests can use the parameter types directly or mock IDurableActivityContext class:
C#
// Copyright (c) .NET Foundation. All rights reserved.// Licensed under the MIT License. See LICENSE in the project root for license information.namespaceVSSample.Tests
{
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Xunit;
using Moq;
publicclassHelloSequenceActivityTests
{
[Fact]
publicvoidSayHello_returns_greeting()
{
var durableActivityContextMock = new Mock<IDurableActivityContext>();
durableActivityContextMock.Setup(x => x.GetInput<string>()).Returns("John");
var result = HelloSequence.SayHello(durableActivityContextMock.Object);
Assert.Equal("Hello John!", result);
}
[Fact]
publicvoidSayHello_returns_greeting_direct_input()
{
var result = HelloSequence.SayHelloDirectInput("John");
Assert.Equal("Hello John!", result);
}
}
}
Pievienojieties meetup sērijai, lai kopā ar citiem izstrādātājiem un ekspertiem izveidotu mērogojamus AI risinājumus, kuru pamatā ir reālas lietošanas gadījumi.