Partilhar via


Teste de unidades

Os testes unitários verificam a lógica de negócio e protegem contra regressões. Orquestrações duradouras coordenam múltiplas atividades e podem tornar-se complexas rapidamente. Adicionar testes unitários ajuda-te a detetar erros cedo.

Com o Durable Functions, pode testar orquestradores, atividades e funções cliente (trigger) ao simular os objetos de contexto fornecidos pelo framework e chamar as suas funções diretamente. Esta abordagem isola a sua lógica de negócio do runtime do Azure Functions.

Os Durable Task SDKs autónomos fornecem infraestrutura de teste incorporada que executa orquestrações em memória sem dependências externas. Regista orquestradores e atividades com um trabalhador de teste, agenda orquestrações através de um cliente de teste e verificas os resultados. Não é preciso mockagem para C# e JavaScript. Python utiliza uma abordagem baseada em executor com eventos de histórico simulado.

Pré-requisitos

  • xUnit — estrutura de testes
  • Moq — framework de simulação
  • Familiaridade com o modelo de trabalhador isolado .NET
  • xUnit — estrutura de testes
  • O pacote Microsoft.DurableTask.InProcessTestHost NuGet

Funções do orquestrador de testes

As funções do orquestrador coordenam atividades, temporizadores e eventos externos. Normalmente contêm mais lógica de negócio e beneficiam mais dos testes unitários.

Simule o contexto de orquestração para controlar os valores de retorno das invocações de atividade. Depois liga diretamente ao teu orquestrador e verifica 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 definir valores de retorno esperados para cada chamada de atividade:

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

    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]);
}

Uso DurableTaskTestHost para executar orquestrações em memória. Regista o teu orquestrador de produção e as classes de atividades, agenda uma orquestração e valida o resultado.

Dadas 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}!");
    }
}

Registe-os diretamente no hospedeiro 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 motor completo de orquestração em memória. Não são necessários serviços externos nem 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. São o tipo de função mais simples de testar porque não têm um comportamento de repetição específico do framework.

As funções de atividade em Azure Functions recebem uma entrada e, opcionalmente, uma 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 o ID de orquestração e o ID da tarefa, mas a maioria dos testes não precisa deles.

Usando a SayHelloActivity classe do exemplo do orquestrador, chame RunAsync diretamente com um contexto simulado:

[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 usas DurableTaskTestHost, as atividades também são executadas como parte do teste de orquestração. Não precisas de testes de atividade separados a menos que a atividade tenha lógica complexa.

Funções do cliente de teste

As funções de cliente (também chamadas funções de gatilho) iniciam orquestrações e gerenciam instâncias. Usam a vinculação durável do cliente para interagir com o motor 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);
}

Mock DurableTaskClient para retornar um ID de instância conhecido.

[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);
}