使用 Visual Studio Code 从 Azure Logic Apps 标准工作流运行中创建单元测试(预览版)

适用于:Azure 逻辑应用(标准)

备注

此功能为预览版,受 Azure 预览版补充使用条款的约束。

单元测试是使应用或解决方案在整个软件开发生命周期内保持可靠且准确的基本做法。 单元测试可帮助你高效、系统地验证解决方案中的关键组件。

对于标准逻辑应用工作流,可以使用 Visual Studio Code 和 Azure 逻辑应用(标准)扩展创建单元测试。 此功能允许你使用以前执行的工作流运行来创建单元测试,并根据逻辑应用解决方案支持的方案定制它们。 此方法提供以下好处:

  • 重用工作流进行操作,生成工作流中特定操作所需的模拟数据。

    此数据允许你测试工作流,而无需调用外部服务、系统或 API。 节省时间,工作流与实际工作流执行方案保持一致。

  • 通过在部署到其他环境之前识别和解决潜在问题来提高工作流质量。

  • 在简化单元测试与开发流程集成的同时,确保工作流行为的一致性和准确性。

本指南演示如何从工作流运行创建单元测试定义。 此定义模拟来自每个工作流作的外部调用,而无需更改工作流逻辑。 从工作流运行创建单元测试时,将获得包含两个文件夹的单元测试项目:

  • 一个文件夹,包含工作流中每个可模拟操作的强类型类。

  • 每个单元测试定义的文件夹,其中包括以下文件:

    • 一个 JSON 文件,表示工作流中生成的模拟操作。

    • 一个 C# 文件,其中包含用于设置自己的断言的示例类和方法,确认工作流的行为符合预期,并确保工作流在更大的 Azure 生态系统中可靠且可预测。

先决条件

限制和已知问题

  • 此版本目前仅支持 C# 来创建单元测试。

  • 此版本不支持非模拟操作。 请确保模拟工作流执行路径中的所有操作。

  • 此版本不支持以下操作类型:

    • 集成帐户操作
    • 数据映射器操作
    • 自定义代码操作
    • XML操作
    • Liquid 操作
    • EDI 编码和解码操作

复习基本概念

以下列表包含有关标准工作流单元测试的基本但重要概念:

  • 逻辑应用单元测试

    注入模拟对象的受控工作流执行。 这些对象表示工作流触发器或依赖于外部服务或系统的动作。

  • 可模拟操作

    依赖于外部服务或系统的工作流操作。 可以将这些操作转换为模拟操作,用于创建和执行单元测试。

从运行的工作流创建单元测试

  1. 在 Visual Studio Code 中,打开标准逻辑应用项目。

  2. 在 Visual Studio Code 工具栏的 “运行 ”菜单中,选择“ 开始调试”。 (键盘:按 F5)

  3. 返回到 “资源管理器” 窗口。 在您的项目中,展开工作流定义文件夹。

  4. 打开 workflow.json 快捷菜单,然后选择“ 概述”。

  5. 在概述页中,在运行历史记录下,选择用于创建单元测试的工作流运行。

    屏幕截图显示了 Visual Studio Code,其中包含标准逻辑应用项目、调试模式运行、打开的工作流概述页和所选工作流运行。

  6. 在运行历史记录工具栏上,选择“从运行创建单元测试”。

    屏幕截图显示 Visual Studio Code、标准工作流运行历史记录页和用于创建单元测试的选定命令。

  7. 提供用于单元测试、单元测试类和 C# 文件的名称。

    “资源管理器” 窗口中,逻辑应用项目文件夹下会显示一个名为 “测试 ”的新项目文件夹。 测试文件夹包含以下文件夹和文件:

    屏幕截图显示了包含单元测试文件夹和文件的 Visual Studio Code、标准逻辑应用项目和测试文件夹。

    文件夹或文件 DESCRIPTION
    Tests
    || <logic-app-name>
    Tests 文件夹中,向逻辑应用项目添加单元测试时,将显示一个 <logic-app-name> 文件夹。
    Tests
    || <logic-app-name>
    ||| <workflow-name>
    <logic-app-name> 文件夹中,为工作流添加单元测试时,将显示一个 <workflow-name> 文件夹。
    Tests
    || <logic-app-name>
    ||| <workflow-name>
    |||| MockOutputs
    < operation-name-outputs >|||||cs
    <workflow-name>文件夹中,MockOutputs文件夹包含一个C#(.cs)文件,该文件包含了工作流中每个连接器操作的强类型类。 每个 .cs 文件名都使用以下格式:

    < operation-name >[Trigger\|Action]Output.cs

    如果连接器操作具有 动态协定,则每个 动态类型会出现一个类。 动态类型是指基于为该参数提供的值具有不同输入和输出的作参数。 可以使用这些类扩展单元测试并从头开始创建新的模拟。
    Tests
    || <logic-app-name>
    ||| <workflow-name>
    |||| <unit-test-name>
    ||||| <unit-test-name>-mock.json
    ||||| <unit-test-name>.cs
    <workflow-name> 文件夹中, <unit-test-name> 该文件夹包含以下文件:

    - <unit-test-name>-mock.json 文件包含生成的模拟的 JSON 表示形式,具体取决于创建单元测试的工作流运行。

    <unit-test-name>.cs- 该文件包含一个示例 C# 类和方法,该方法使用*-mock.json该文件来运行和断言结果。 可以编辑此文件以匹配特定的测试方案。

查看 *-mock.json 文件

此文件包含以下主要部分:

triggerMocks 部分

triggerMocks 部分包含工作流触发器中的模拟结果。 若要启动工作流执行,需要此部分,如以下示例所示:

{
    "triggerMocks": {
        "When_messages_are_available_in_a_queue_(peek-lock)": {
            "name": "When_messages_are_available_in_a_queue_(peek-lock)",
            "status": "Succeeded",
            "outputs": {
                "body": {
                    "contentData": {
                        "messageId": "1234",
                        "status": "new",
                        "contentType": "application/json",
                        "userProperties": {},
                        "scheduledEnqueueTimeUtc": "1/1/0001 12:00:00 AM",
                        "timeToLive": "14.00:00:00",
                        "deliveryCount": 1,
                        "enqueuedSequenceNumber": 0,
                        "enqueuedTimeUtc": "2025-04-07T01:10:09.738Z",
                        "lockedUntilUtc": "2025-04-07T01:11:09.769Z",
                        "lockToken": "78232fa8-03cf-4baf-b1db-3375a64e0ced",
                        "sequenceNumber": 5
                    }
                }
            }
        }
    },
    "actionMocks": {...}
}

actionMocks 部分

对于工作流运行中的每个可模拟动作,actionMocks部分包含一个模拟动作,并保证工作流的受控执行。

{
    "triggerMocks": {...},
    "actionMocks": {
        "Call_External_API": {
            "name": "Call_External_API",
            "status": "Succeeded",
            "outputs": {
                "statusCode": 200,
                "body": {
                    "status": "Awesome!"
                }
            }
        },
        "CompleteMessage": {
            "name": "CompleteMessage",
            "status": "Succeeded",
            "outputs": {
                "statusCode": "OK",
                "body": {}
            }
        }
    }
}

查看单元测试 *.cs 文件

此单元测试类提供一个框架,用于通过模拟触发器和动作来测试标准逻辑应用工作流。 此类允许你测试工作流,而无需实际调用外部服务或 API。

测试类结构

典型的单元测试类使用以下结构:

[TestClass]
public class <unit-test-name>
{
    public TestExecutor TestExecutor;

    [TestInitialize]
    public void Setup()
    {
        this.TestExecutor = new TestExecutor("<workflow-name>/testSettings.config");
    }

    // Add test methods here.

    // Add helper methods here.
}

Setup() 方法

此方法使用测试设置配置文件的路径实例化 TestExecutor 类。 该方法在每次测试执行之前运行,并创建一个新实例 TestExecutor

[TestInitialize]
public void Setup()
{
    this.TestExecutor = new TestExecutor("<workflow-name>/testSettings.config");
}

示例测试方法

以下部分介绍可在单元测试类中使用的示例测试方法。

静态模拟数据测试

以下方法演示如何使用静态模拟数据测试工作流。 在此方法中,可以完成以下任务:

  • 在模拟的操作上设置属性值。
  • 使用配置的模拟数据执行工作流。
  • 确认执行成功。
[TestMethod]
public async Task <workflow-name>_<unit-test-name>_ExecuteWorkflow_SUCCESS_Sample1()
{
    // PREPARE mock: Generate mock action and trigger data.
    var mockData = this.GetTestMockDefinition();
    var sampleActionMock = mockData.ActionMocks["Call_External_API"];
    sampleActionMock.Outputs["your-property-name"] = "your-property-value";

    // ACT: Create the UnitTestExecutor instance. Run the workflow with mock data.
    var testRun = await this.TestExecutor
        .Create()
        .RunWorkflowAsync(testMock: mockData).ConfigureAwait(continueOnCapturedContext: false);

    // ASSERT: Confirm successful workflow execution and that the status is 'Succeeded'.
    Assert.IsNotNull(value: testRun);
    Assert.AreEqual(expected: TestWorkflowStatus.Succeeded, actual: testRun.Status);
}

动态模拟数据测试

以下方法演示如何将动态模拟数据与回调方法配合使用。 此方法提供两个选项来动态生成模拟数据:

这两种方法都允许基于单元测试执行上下文创建动态响应。

[TestMethod]
public async Task <workflow-name>_<unit-test-name>_ExecuteWorkflow_SUCCESS_Sample2()
{
    // PREPARE: Generate mock action and trigger data.
    var mockData = this.GetTestMockDefinition();
    
    // OPTION 1: Define a callback class.
    mockData.ActionMocks["Call_External_API"] = new CallExternalAPIActionMock(
        name: "Call_External_API", 
        onGetActionMock: CallExternalAPIActionMockOutputCallback);

    // OPTION 2: Define an inline lambda function.
    mockData.ActionMocks["Call_External_API"] = new CallExternalAPIActionMock(
        name: "Call_External_API", 
        onGetActionMock: (testExecutionContext) =>
        {
            return new CallExternalAPIActionMock(
                status: TestWorkflowStatus.Succeeded,
                outputs: new CallExternalAPIActionOutput {

                    // If this account contains a JObject Body, 
                    // set the properties you want here:
                    // Body = "something".ToJObject()

                }
            );
        });
        
    // ACT: Create UnitTestExecutor instance. Run the workflow with mock data.
    var testRun = await this.TestExecutor
        .Create()
        .RunWorkflowAsync(testMock: mockData).ConfigureAwait(continueOnCapturedContext: false);

    // ASSERT: Confirm successful workflow execution and that the status is 'Succeeded'.
    Assert.IsNotNull(value: testRun);
    Assert.AreEqual(expected: TestWorkflowStatus.Succeeded, actual: testRun.Status);
}

错误方案测试

以下方法演示如何测试失败条件。 在此方法中,可以完成以下任务:

  • 配置模拟操作以失败并显示特定的错误代码和消息。
  • 确认工作流正确处理这些错误条件。
[TestMethod]
public async Task <workflow-name>_<unit-test-name>_ExecuteWorkflow_FAILED_Sample3()
{
    // PREPARE: Generate mock action and trigger data.
    var mockData = this.GetTestMockDefinition();
    var mockError = new TestErrorInfo(code: ErrorResponseCode.BadRequest, message: "Input is invalid.");
    mockData.ActionMocks["Call_External_API"] = new CallExternalAPIActionMock(
        status: TestWorkflowStatus.Failed, 
        error: mockError);

    // ACT: Create UnitTestExecutor instance. Run the workflow with mock data.
    var testRun = await this.TestExecutor
        .Create()
        .RunWorkflowAsync(testMock: mockData).ConfigureAwait(continueOnCapturedContext: false);

    // ASSERT: Confirm successful workflow execution and that the status is 'Succeeded'.
    Assert.IsNotNull(value: testRun);
    Assert.AreEqual(expected: TestWorkflowStatus.Failed, actual: testRun.Status);
}

辅助方法

以下部分介绍示例测试方法使用的方法。 帮助程序方法显示在类定义中的测试方法下。

GetTestMockDefinition()

以下方法从 JSON 文件加载模拟定义。 如果模拟数据存储在不同的位置或格式,则可以编辑此方法。

private TestMockDefinition GetTestMockDefinition()
{
    var mockDataPath = Path.Combine(TestExecutor.rootDirectory, "Tests", TestExecutor.logicAppName, 
        TestExecutor.workflow, "<unit-test-name>", "<unit-test-name>-mock.json");
    return JsonConvert.DeserializeObject<TestMockDefinition>(File.ReadAllText(mockDataPath));
}

回调方法

以下方法动态生成模拟数据。 方法名称根据测试方法中用于静态或动态模拟数据的模拟动作名称而有所不同。 可以编辑此方法以基于测试方案要求返回不同的模拟响应,或者将其用作模板来创建自己的动态回调方法。

public CallExternalAPIActionMock CallExternalAPIActionMockOutputCallback(TestExecutionContext context)
{
    // Sample mock data: Dynamically change the mocked data for "actionName".
    return new CallExternalAPIActionMock(
        status: TestWorkflowStatus.Succeeded,
        outputs: new CallExternalAPIActionOutput {

            // If this account contains a JObject Body, 
            // set the properties you want here:
            // Body = "something".ToJObject()

        }
    );
}

使用 Visual Studio Code 从 Azure 逻辑应用中的标准工作流定义创建单元测试