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 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 por meio de um cliente de teste e afirme sobre os resultados. Nenhum mock é 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.

Simular o contexto de orquestração para controlar os valores retornados das 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. Nenhum serviço externo ou processos do tipo sidecar são necessários.

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 a ser testado porque não têm nenhum 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. Eles 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);
}