Поделиться через


Модульное тестирование

Модульные тесты проверяют бизнес-логику и защищают от регрессий. Устойчивые оркестрации координируют различные процессы и могут быстро наращивать сложность. Добавление модульных тестов помогает ловить ошибки раньше.

С помощью Durable Functions вы тестируете функции оркестратора, действий и клиента (триггера), имитируя объекты контекста, предоставляемые платформой и вызывая ваши функции напрямую. Этот подход изолирует бизнес-логику от среды выполнения Azure Functions.

Автономные пакеты SDK для устойчивых задач предоставляют встроенную тестовую инфраструктуру , которая выполняет оркестрации в памяти без внешних зависимостей. Вы регистрируете оркестраторы и действия у тестового работника, планируете оркестрации с помощью тестового клиента и проверяете результаты. Для C# и JavaScript не требуется мокирование. Python использует подход на основе исполнителя с событиями имитации истории.

Необходимые условия

  • xUnit — тестовая платформа
  • Microsoft.DurableTask.InProcessTestHost Пакет NuGet

Тестирование функций оркестратора

Функции оркестратора координируют действия, таймеры и внешние события. Обычно они содержат большую бизнес-логику и получают максимальную выгоду от модульного тестирования.

Макет контекста оркестрации для управления возвращаемыми значениями вызовов действий. Затем вызовите оркестратор напрямую и проверьте выходные данные.

Рассмотрим этот оркестратор, который вызывает действие три раза:

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

Используйте Moq для создания имитации TaskOrchestrationContext и настройки ожидаемых возвращаемых значений для каждого вызова функции.

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

Используйте DurableTaskTestHost для выполнения оркестраций в памяти. Зарегистрируйте производственный оркестратор и классы активности, запланируйте оркестрацию и проверьте результат.

Учитывая следующие производственные классы:

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

Зарегистрируйте их непосредственно на тестовом узле:

[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 запускает полный модуль оркестрации в памяти. Не требуются внешние службы или дополнительные процессы.

Функции тестового действия

Функции действий содержат фактическую работу— вызов API, обработку данных или взаимодействие с внешними системами. Это самый простой тип функции для тестирования, так как они не имеют поведения воспроизведения, специфичного для конкретного фреймворка.

Функции действий в Azure Functions получают входные данные и при необходимости FunctionContext. Проверьте их как любую другую функцию:

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

Функции действия получают объект контекста и входные данные. Контекст предоставляет метаданные, такие как идентификатор оркестрации и идентификатор задачи, но большинству тестов это не нужно.

SayHelloActivity Используя класс из примера оркестратора, вызовите RunAsync непосредственно с помощью контекста макета:

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

При использовании DurableTaskTestHost действия также выполняются в рамках теста оркестрации. Вам не нужны отдельные тесты действий, если действие не имеет сложной логики.

Тестирование клиентских функций

Клиентские функции (также называемые функциями триггера) запускают процессы оркестрации и управляют их экземплярами. Они используют устойчивую привязку клиента для взаимодействия с подсистемой оркестрации.

Рассмотрим этот триггер HTTP, который запускает оркестрацию:

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

Макет DurableTaskClient для возврата известного идентификатора экземпляра:

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