Teste de unidade de Durable Functions e SDKs de Tarefas Duráveis

O teste de unidade de orquestrações duráveis ajuda você a verificar a lógica de negócios e detectar erros antecipadamente. As orquestrações coordenam várias atividades e podem ficar complexas rapidamente, portanto, os testes protegem contra regressões à medida que o fluxo de trabalho evolui.

Selecione a guia que corresponde ao seu projeto: Durable Functions se você usar Azure Functions, ou SDKs de Tarefas Duráveis se utilizar o SDK autônomo sem Azure Functions.

Com o Durable Functions, você testa orquestradores, atividades e funções de cliente (gatilho) simulando os objetos de contexto fornecidos pela estrutura e chamando suas funções diretamente. Essa abordagem isola sua lógica de negócios do runtime do Azure Functions.

Aqui está um teste mínimo do orquestrador em C# para mostrar o padrão:

[Fact]
public async Task MyOrchestrator_CallsActivity()
{
    var contextMock = new Mock<TaskOrchestrationContext>();
    contextMock.Setup(x => x.CallActivityAsync<string>(
        It.IsAny<TaskName>(), It.IsAny<string>(), It.IsAny<TaskOptions>()))
        .ReturnsAsync("result");

    var result = await MyOrchestrator.Run(contextMock.Object);

    Assert.Equal("result", result);
}

O restante deste artigo aborda esse padrão em detalhes para C# e Python.

Os SDKs de Tarefa Duráveis autônomos fornecem uma infraestrutura de teste interna que executa orquestrações na memória sem dependências externas. Registre orquestradores e atividades com um trabalho de teste, agende orquestrações usando um cliente de teste e afirme sobre os resultados. Simulação necessário para C# e JavaScript. Python usa uma abordagem baseada em gerador com injeção de resultado manual.

Aqui está um teste de C# mínimo para mostrar o padrão:

[Fact]
public async Task MyOrchestrator_Completes()
{
    await using var host = await DurableTaskTestHost.StartAsync(tasks =>
    {
        tasks.AddOrchestrator<MyOrchestrator>();
        tasks.AddActivity<MyActivity>();
    });

    string id = await host.Client.ScheduleNewOrchestrationInstanceAsync(nameof(MyOrchestrator));
    var result = await host.Client.WaitForInstanceCompletionAsync(id, getInputsAndOutputs: true);

    Assert.Equal(OrchestrationRuntimeStatus.Completed, result.RuntimeStatus);
}

O restante deste artigo aborda esse padrão em detalhes para C#, Python e JavaScript.

Pré-requisitos

Testar funções de um orquestrador

As funções de orquestrador coordenam atividades, temporizadores e eventos externos. Normalmente, elas contêm mais lógica de negócios e se beneficiam mais do teste de unidade.

Simule o contexto de orquestração para controlar os valores de retorno de chamadas de atividade. Em seguida, chame o orquestrador diretamente e verifique a saída.

Considere este orquestrador que chama uma atividade três vezes:

[Function(nameof(HelloCitiesOrchestration))]
public static async Task<List<string>> HelloCities(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var outputs = new List<string>
    {
        await context.CallActivityAsync<string>(nameof(SayHello), "Tokyo"),
        await context.CallActivityAsync<string>(nameof(SayHello), "Seattle"),
        await context.CallActivityAsync<string>(nameof(SayHello), "London")
    };

    return outputs;
}

Use o Moq para simular TaskOrchestrationContext e configurar valores retornados esperados para cada chamada de atividade.

Note

O It.Is<TaskName>(...) padrão é necessário porque CallActivityAsync aceita um TaskName struct, não uma cadeia de caracteres simples. Moq precisa da correspondência explícita de tipo.

[Fact]
public async Task HelloCities_ReturnsExpectedGreetings()
{
    var contextMock = new Mock<TaskOrchestrationContext>();

    // Mock each activity call to return a known value
    contextMock.Setup(x => x.CallActivityAsync<string>(
        It.Is<TaskName>(n => n.Name == nameof(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(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(SayHello)),
        It.Is<string>(n => n == "London"),
        It.IsAny<TaskOptions>())).ReturnsAsync("Hello London!");

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

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

Use DurableTaskTestHost para executar orquestrações na memória. Registre seu orquestrador de produção e as classes de atividade, agende uma orquestração e verifique o resultado.

Considerando estas classes de produção:

class HelloCitiesOrchestrator : TaskOrchestrator<string, List<string>>
{
    public override async Task<List<string>> RunAsync(
        TaskOrchestrationContext context, string input)
    {
        var outputs = new List<string>
        {
            await context.CallActivityAsync<string>(nameof(SayHelloActivity), "Tokyo"),
            await context.CallActivityAsync<string>(nameof(SayHelloActivity), "Seattle"),
            await context.CallActivityAsync<string>(nameof(SayHelloActivity), "London")
        };
        return outputs;
    }
}

class SayHelloActivity : TaskActivity<string, string>
{
    public override Task<string> RunAsync(TaskActivityContext context, string name)
    {
        return Task.FromResult($"Hello {name}!");
    }
}

Registre-os diretamente no host de teste:

[Fact]
public async Task HelloCities_ReturnsExpectedGreetings()
{
    await using var host = await DurableTaskTestHost.StartAsync(tasks =>
    {
        tasks.AddOrchestrator<HelloCitiesOrchestrator>();
        tasks.AddActivity<SayHelloActivity>();
    });

    string instanceId = await host.Client.ScheduleNewOrchestrationInstanceAsync(
        nameof(HelloCitiesOrchestrator));
    OrchestrationMetadata result = await host.Client.WaitForInstanceCompletionAsync(
        instanceId, getInputsAndOutputs: true);

    Assert.Equal(OrchestrationRuntimeStatus.Completed, result.RuntimeStatus);

    var output = result.ReadOutputAs<List<string>>();
    Assert.Equal(3, output.Count);
    Assert.Equal("Hello Tokyo!", output[0]);
    Assert.Equal("Hello Seattle!", output[1]);
    Assert.Equal("Hello London!", output[2]);
}

DurableTaskTestHost executa um mecanismo de orquestração completo em memória. Não são necessários serviços externos ou processos sidecar.

Funções de atividade de teste

As funções de atividade contêm o trabalho real : chamar APIs, processar dados ou interagir com sistemas externos. Eles são o tipo de função mais simples para teste porque não têm comportamento de reprodução específico da estrutura.

As funções de atividade em Azure Functions recebem uma entrada e, opcionalmente, um FunctionContext. Teste-os como qualquer outra função:

[Function(nameof(SayHello))]
public static string SayHello(
    [ActivityTrigger] string name, FunctionContext executionContext)
{
    return $"Hello {name}!";
}
[Fact]
public void SayHello_ReturnsExpectedGreeting()
{
    var result = HelloCitiesOrchestration.SayHello("Tokyo", Mock.Of<FunctionContext>());
    Assert.Equal("Hello Tokyo!", result);
}

As funções de atividade recebem um objeto de contexto e uma entrada. O contexto fornece metadados como a ID de orquestração e a ID da tarefa, mas a maioria dos testes não precisa dele.

Usando a SayHelloActivity classe do exemplo de orquestrador, chame RunAsync diretamente com um contexto fictício:

[Fact]
public async Task SayHello_ReturnsExpectedGreeting()
{
    var activity = new SayHelloActivity();
    var contextMock = new Mock<TaskActivityContext>();

    var result = await activity.RunAsync(contextMock.Object, "Tokyo");

    Assert.Equal("Hello Tokyo!", result);
}

Quando você usa DurableTaskTestHost, as atividades também são executadas como parte do teste de orquestração. Você não precisa de testes de atividade separados, a menos que a atividade tenha uma lógica complexa.

Testar funções de cliente

As funções de cliente (também chamadas de funções de gatilho) iniciam orquestrações e gerenciam instâncias. Elas usam a associação de cliente durável para interagir com o mecanismo de orquestração.

Considere este gatilho HTTP que inicia uma orquestração:

[Function("HelloCitiesOrchestration_HttpStart")]
public static async Task<HttpResponseData> HttpStart(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
    [DurableClient] DurableTaskClient client,
    FunctionContext executionContext)
{
    string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
        nameof(HelloCitiesOrchestration));
    return await client.CreateCheckStatusResponseAsync(req, instanceId);
}

Simular DurableTaskClient para retornar uma ID de instância conhecida:

[Fact]
public async Task HttpStart_ReturnsAccepted()
{
    var durableClientMock = new Mock<DurableTaskClient>("testClient");
    var functionContextMock = new Mock<FunctionContext>();
    var instanceId = "test-instance-id";

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

    var mockRequest = CreateMockHttpRequest(functionContextMock.Object);

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

    durableClientMock
        .Setup(x => x.CreateCheckStatusResponseAsync(
            It.IsAny<HttpRequestData>(),
            It.IsAny<string>(),
            It.IsAny<CancellationToken>()))
        .ReturnsAsync(responseMock.Object);

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

    Assert.Equal(HttpStatusCode.Accepted, result.StatusCode);
}

Testar operações do cliente

Com os SDKs de Tarefa Duráveis autônomos, as operações do cliente (agendando orquestrações, consultando status, gerando eventos) usam o mesmo TestOrchestrationClient já mostrado nos testes do orquestrador. Não existe nenhuma função de cliente separada – você chama a API do cliente diretamente.

DurableTaskTestHost expõe host.Client, que é um DurableTaskClient totalmente funcional. Use-o para testar operações no nível do cliente, como agendamento, consulta ou encerramento de orquestrações.

[Fact]
public async Task Client_CanQueryOrchestrationStatus()
{
    await using var host = await DurableTaskTestHost.StartAsync(tasks =>
    {
        tasks.AddOrchestrator<HelloCitiesOrchestrator>();
        tasks.AddActivity<SayHelloActivity>();
    });

    string instanceId = await host.Client.ScheduleNewOrchestrationInstanceAsync(
        nameof(HelloCitiesOrchestrator));

    // Query status while the orchestration runs
    OrchestrationMetadata metadata = await host.Client.WaitForInstanceCompletionAsync(
        instanceId, getInputsAndOutputs: true);

    Assert.Equal(OrchestrationRuntimeStatus.Completed, metadata.RuntimeStatus);
    Assert.Equal(instanceId, metadata.InstanceId);
}