Durable Functions 单元测试Durable Functions unit testing

单元测试是现代软件开发实践中的重要组成部分。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 的复杂性很容易增大,因此,引入单元测试有助于避免中断性变更。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.

备注

本文提供了针对 Durable Functions 1.x 的 Durable Functions 应用的单元测试指南。This article provides guidance for unit testing for Durable Functions apps targeting Durable Functions 1.x. 它尚未更新,以考虑到 Durable Functions 2.x 中引入的更改。It has not yet been updated to account for changes introduced in Durable Functions 2.x. 有关版本之间差异的详细信息,请参阅 Durable Functions 版本一文。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:

  • 单元测试Unit testing

  • Durable FunctionsDurable Functions

  • xUnit - 测试框架xUnit - Testing framework

  • moq - 模拟框架moq - Mocking framework

用于模拟的基类Base classes for mocking

通过 Durable Functions 1.x 中的三个抽象类来支持模拟:Mocking is supported via three abstract classes in Durable Functions 1.x:

  • DurableOrchestrationClientBase

  • DurableOrchestrationContextBase

  • DurableActivityContextBase

这些类是定义业务流程客户端、业务流程协调程序和活动方法的 DurableOrchestrationClientDurableOrchestrationContextDurableActivityContext 的基类。These classes are base classes for DurableOrchestrationClient, DurableOrchestrationContext, and DurableActivityContext that define Orchestration Client, Orchestrator, and Activity methods. 模拟将会设置基类方法的预期行为,使单元测试能够验证业务逻辑。The mocks will set expected behavior for base class methods so the unit test can verify the business logic. 可以通过一个两步工作流对业务流程客户端和业务流程协调程序中的业务逻辑进行单元测试:There is a two-step workflow for unit testing the business logic in the Orchestration Client and Orchestrator:

  1. 定义业务流程客户端和业务流程协调程序函数签名时,使用基类而不是具体的实现。Use the base classes instead of the concrete implementation when defining orchestration client and orchestrator function signatures.
  2. 在单元测试中模拟基类的行为,并验证业务逻辑。In the unit tests mock the behavior of the base classes and verify the business logic.

在以下段落中,可以找到有关对使用业务流程客户端绑定和业务流程协调程序触发器绑定的函数进行测试的更多详细信息。Find more details in the following paragraphs for testing functions that use the orchestration client binding and the orchestrator trigger binding.

对触发器函数进行单元测试Unit testing trigger functions

在此部分,单元测试将会验证用于启动新业务流程的以下 HTTP 触发器函数的逻辑。In this section, the unit test will validate the logic of the following HTTP trigger function for starting new orchestrations.

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;

namespace VSSample
{
    public static class HttpStart
    {
        [FunctionName("HttpStart")]
        public static async Task<HttpResponseMessage> Run(
            [HttpTrigger(AuthorizationLevel.Function, methods: "post", Route = "orchestrators/{functionName}")] HttpRequestMessage req,
            [OrchestrationClient] DurableOrchestrationClientBase starter,
            string functionName,
            ILogger log)
        {
            // Function input comes from the request content.
            dynamic 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);
        }
    }
}

单元测试任务是验证响应有效负载中提供的 Retry-After 标头的值。The unit test task will be to verify the value of the Retry-After header provided in the response payload. 因此,单元测试将模拟某些 DurableOrchestrationClientBase 方法,以确保行为可预测。So the unit test will mock some of DurableOrchestrationClientBase methods to ensure predictable behavior.

首先,需要模拟基类 DurableOrchestrationClientBaseFirst, a mock of the base class is required, DurableOrchestrationClientBase. 该模拟可以是实现 DurableOrchestrationClientBase 的新类。The mock can be a new class that implements DurableOrchestrationClientBase. 但是,使用 moq 之类的模拟框架可以简化过程:However, using a mocking framework like moq simplifies the process:

    // Mock DurableOrchestrationClientBase
    var durableOrchestrationClientBaseMock = new Mock<DurableOrchestrationClientBase>();

然后,模拟 StartNewAsync 方法来返回已知的实例 ID。Then StartNewAsync method is mocked to return a well-known instance ID.

    // Mock StartNewAsync method
    durableOrchestrationClientBaseMock.
        Setup(x => x.StartNewAsync(functionName, It.IsAny<object>())).
        ReturnsAsync(instanceId);

接下来模拟 CreateCheckStatusResponse,以便始终返回空白的 HTTP 200 响应。Next CreateCheckStatusResponse is mocked to always return an empty HTTP 200 response.

    // Mock CreateCheckStatusResponse method
    durableOrchestrationClientBaseMock
        .Setup(x => x.CreateCheckStatusResponse(It.IsAny<HttpRequestMessage>(), instanceId))
        .Returns(new HttpResponseMessage
        {
            StatusCode = HttpStatusCode.OK,
            Content = new StringContent(string.Empty),
            Headers =
            {
                RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10))
            }
        });

另外还要模拟 ILoggerILogger is also mocked:

    // Mock ILogger
    var loggerMock = new Mock<ILogger>();

现在,从单元测试调用 Run 方法:Now the Run method is called from the unit test:

    // Call Orchestration trigger function
    var result = await HttpStart.Run(
        new HttpRequestMessage()
        {
            Content = new StringContent("{}", Encoding.UTF8, "application/json"),
            RequestUri = new Uri("http://localhost:7071/orchestrators/E1_HelloSequence"),
        },
        durableOrchestrationClientBaseMock.Object,
        functionName,
        loggerMock.Object);

最后一步是将输出与预期值进行比较:The last step is to compare the output with the expected value:

    // 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:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace VSSample.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;
    using Microsoft.Extensions.Logging;
    using Moq;
    using Xunit;

    public class HttpStartTests
    {
        [Fact]
        public async Task HttpStart_returns_retryafter_header()
        {
            // Define constants
            const string functionName = "SampleFunction";
            const string instanceId = "7E467BDB-213F-407A-B86A-1954053D3C24";

            // Mock TraceWriter
            var loggerMock = new Mock<ILogger>();

            // Mock DurableOrchestrationClientBase
            var durableOrchestrationClientBaseMock = new Mock<DurableOrchestrationClientBase>();

            // Mock StartNewAsync method
            durableOrchestrationClientBaseMock.
                Setup(x => x.StartNewAsync(functionName, It.IsAny<object>())).
                ReturnsAsync(instanceId);

            // Mock CreateCheckStatusResponse method
            durableOrchestrationClientBaseMock
                .Setup(x => x.CreateCheckStatusResponse(It.IsAny<HttpRequestMessage>(), instanceId))
                .Returns(new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent(string.Empty),
                    Headers =
                    {
                        RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10))
                    }
                });

            // Call Orchestration trigger function
            var result = await HttpStart.Run(
                new HttpRequestMessage()
                {
                    Content = new StringContent("{}", Encoding.UTF8, "application/json"),
                    RequestUri = new Uri("http://localhost:7071/orchestrators/E1_HelloSequence"),
                },
                durableOrchestrationClientBaseMock.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.

在本部分,单元测试将验证 E1_HelloSequence 业务流程协调程序函数的输出:In this section the unit tests will validate the output of the E1_HelloSequence Orchestrator function:

// 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;

namespace VSSample
{
    public static class HelloSequence
    {
        [FunctionName("E1_HelloSequence")]
        public static async Task<List<string>> Run(
            [OrchestrationTrigger] DurableOrchestrationContextBase 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")]
        public static string SayHello([ActivityTrigger] DurableActivityContextBase context)
        {
            string name = context.GetInput<string>();
            return $"Hello {name}!";
        }

        [FunctionName("E1_SayHello_DirectInput")]
        public static string SayHelloDirectInput([ActivityTrigger] string name)
        {
            return $"Hello {name}!";
        }
    }
 }

单元测试代码首先会创建模拟:The unit test code will start with creating a mock:

    var durableOrchestrationContextMock = new Mock<DurableOrchestrationContextBase>();

然后模拟活动方法调用:Then the activity method calls will be mocked:

    durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Tokyo")).ReturnsAsync("Hello Tokyo!");
    durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Seattle")).ReturnsAsync("Hello Seattle!");
    durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "London")).ReturnsAsync("Hello London!");

接下来,单元测试调用 HelloSequence.Run 方法:Next the unit test will call HelloSequence.Run method:

    var result = await HelloSequence.Run(durableOrchestrationContextMock.Object);

最后验证输出:And finally the output will be validated:

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

合并所有步骤后,单元测试将获得以下代码:After combining all steps, the unit test will have the following code:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace VSSample.Tests
{
    using System.Threading.Tasks;
    using Microsoft.Azure.WebJobs;
    using Moq;
    using Xunit;

    public class HelloSequenceTests
    {
        [Fact]
        public async Task Run_returns_multiple_greetings()
        {
            var durableOrchestrationContextMock = new Mock<DurableOrchestrationContextBase>();
            durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Tokyo")).ReturnsAsync("Hello Tokyo!");
            durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Seattle")).ReturnsAsync("Hello Seattle!");
            durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello_DirectInput", "London")).ReturnsAsync("Hello London!");

            var result = await HelloSequence.Run(durableOrchestrationContextMock.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.

在本部分,单元测试将验证 E1_SayHello 活动函数的行为:In this section the unit test will validate the behavior of the E1_SayHello Activity function:

// 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;

namespace VSSample
{
    public static class HelloSequence
    {
        [FunctionName("E1_HelloSequence")]
        public static async Task<List<string>> Run(
            [OrchestrationTrigger] DurableOrchestrationContextBase 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")]
        public static string SayHello([ActivityTrigger] DurableActivityContextBase context)
        {
            string name = context.GetInput<string>();
            return $"Hello {name}!";
        }

        [FunctionName("E1_SayHello_DirectInput")]
        public static string SayHelloDirectInput([ActivityTrigger] string name)
        {
            return $"Hello {name}!";
        }
    }
 }

另外,单元测试将验证输出格式。And the unit tests will verify the format of the output. 单元测试可以直接使用参数类型,也可以模拟 DurableActivityContextBase 类:The unit tests can use the parameter types directly or mock DurableActivityContextBase class:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace VSSample.Tests
{
    using Microsoft.Azure.WebJobs;
    using Xunit;
    using Moq;

    public class HelloSequenceActivityTests
    {
        [Fact]
        public void SayHello_returns_greeting()
        {
            var durableActivityContextMock = new Mock<DurableActivityContextBase>();
            durableActivityContextMock.Setup(x => x.GetInput<string>()).Returns("John");
            var result = HelloSequence.SayHello(durableActivityContextMock.Object);
            Assert.Equal("Hello John!", result);
        }

        [Fact]
        public void SayHello_returns_greeting_direct_input()
        {
            var result = HelloSequence.SayHelloDirectInput("John");
            Assert.Equal("Hello John!", result);
        }
    }
}

后续步骤Next steps