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


Модульное тестирование с помощью Orleans

В этом руководстве показано, как проводить модульное тестирование ваших зерен, чтобы обеспечить правильность их поведения. Существует два основных способа модульного тестирования зерна, и выбранный метод зависит от типа функциональности, которую вы тестируете. Используйте пакет Microsoft.Orleans.TestingHost NuGet для создания тестовых силосов для ваших зерен или фреймворка для создания макетов, такого как Moq, для имитации частей Orleans среды выполнения, с которой взаимодействуют ваши зерна.

Рекомендуемая InProcessTestCluster инфраструктура тестирования для Orleans. Он предоставляет упрощенный API, основанный на делегатах, для настройки тестовых кластеров, что упрощает совместное использование служб между вашими тестами и кластером.

Основные преимущества

Основным преимуществом InProcessTestCluster, по сравнению с TestCluster, является эргономика:

  • Конфигурация, основанная на делегатах: настройка силосов и клиентов, используя встроенные делегаты вместо отдельных классов конфигурации
  • Экземпляры общих служб: легко делиться имитирующими службами, тестовыми дублёрами и другими экземплярами между вашим тест-кодом и узлами хранилища.
  • Меньше шаблонов: нет необходимости создавать отдельные ISiloConfigurator или IClientConfigurator классы
  • Упрощённое внедрение зависимостей: регистрация служб непосредственно в гибком API построителя

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

Базовое использование

using Orleans.TestingHost;
using Xunit;

public class HelloGrainTests : IAsyncLifetime
{
    private InProcessTestCluster _cluster = null!;

    public async Task InitializeAsync()
    {
        var builder = new InProcessTestClusterBuilder();
        _cluster = builder.Build();
        await _cluster.DeployAsync();
    }

    public async Task DisposeAsync()
    {
        await _cluster.DisposeAsync();
    }

    [Fact]
    public async Task SaysHello()
    {
        var grain = _cluster.Client.GetGrain<IHelloGrain>(0);
        var result = await grain.SayHello("World");
        Assert.Equal("Hello, World!", result);
    }
}

Настройка тестового кластера

Используйте InProcessTestClusterBuilder для настройки силосов, клиентов и служб:

var builder = new InProcessTestClusterBuilder(initialSilosCount: 2);

// Configure silos
builder.ConfigureSilo((options, siloBuilder) =>
{
    siloBuilder.AddMemoryGrainStorage("Default");
    siloBuilder.AddMemoryGrainStorage("PubSubStore");
});

// Configure clients
builder.ConfigureClient(clientBuilder =>
{
    // Client-specific configuration
});

// Configure both silos and clients (shared services)
builder.ConfigureHost(hostBuilder =>
{
    hostBuilder.Services.AddSingleton<IMyService, MyService>();
});

var cluster = builder.Build();
await cluster.DeployAsync();

InProcessTestClusterOptions

Вариант Тип По умолчанию Description
ClusterId string Автоматическое создание Идентификатор кластера.
ServiceId string Автоматическое создание Идентификатор службы.
InitialSilosCount int 1 Первоначальное число силосов.
InitializeClientOnDeploy bool true Следует ли автоматически инициализировать клиент при развертывании.
ConfigureFileLogging bool true Включите ведение журнала файлов для отладки.
UseRealEnvironmentStatistics bool false Используйте реальную статистику памяти и ЦП вместо имитированных значений.
GatewayPerSilo bool true Имеет ли каждый силос шлюз для клиентских подключений.

Совместное использование тестового кластера между тестами

Чтобы повысить производительность тестов, поделитесь одним кластером в нескольких тестовых случаях с помощью светильников xUnit:

public class ClusterFixture : IAsyncLifetime
{
    public InProcessTestCluster Cluster { get; private set; } = null!;

    public async Task InitializeAsync()
    {
        var builder = new InProcessTestClusterBuilder();
        builder.ConfigureSilo((options, siloBuilder) =>
        {
            siloBuilder.AddMemoryGrainStorageAsDefault();
        });

        Cluster = builder.Build();
        await Cluster.DeployAsync();
    }

    public async Task DisposeAsync()
    {
        await Cluster.DisposeAsync();
    }
}

[CollectionDefinition(nameof(ClusterCollection))]
public class ClusterCollection : ICollectionFixture<ClusterFixture>
{
}

[Collection(nameof(ClusterCollection))]
public class HelloGrainTests
{
    private readonly ClusterFixture _fixture;

    public HelloGrainTests(ClusterFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task SaysHello()
    {
        var grain = _fixture.Cluster.Client.GetGrain<IHelloGrain>(0);
        var result = await grain.SayHello("World");
        Assert.Equal("Hello, World!", result);
    }
}

Добавление и удаление силосов во время тестов

Элемент InProcessTestCluster поддерживает динамическое управление силосами для тестирования поведения кластера.

// Start with 2 silos
var builder = new InProcessTestClusterBuilder(initialSilosCount: 2);
var cluster = builder.Build();
await cluster.DeployAsync();

// Add a third silo
var newSilo = await cluster.StartSiloAsync();

// Stop a silo
await cluster.StopSiloAsync(newSilo);

// Restart all silos
await cluster.RestartAsync();

Используйте TestCluster

Элемент TestCluster использует подход к конфигурации на основе классов, который требует реализации интерфейсов ISiloConfigurator и IClientConfigurator. Этот дизайн поддерживает сценарии многопроцессного тестирования, в которых силосы выполняются в отдельных процессах, что полезно для тестирования, приближенного к производственной среде. Однако по умолчанию TestCluster также выполняется в процессе с эквивалентной производительностью InProcessTestCluster.

Выберите TestCluster вместо InProcessTestCluster в следующих случаях:

  • Требуется многопроцессное тестирование для моделирования рабочей среды
  • У вас есть тесты с помощью TestCluster API
  • Вам нужна совместимость с Orleans 7.x или 8.x

Для новых тестов InProcessTestCluster рекомендуется из-за более простой конфигурации на основе делегатов.

Пакет Microsoft.Orleans.TestingHost NuGet содержит TestCluster, который можно использовать для создания кластера в памяти (состоящий из двух силосов по умолчанию) для тестирования зерна.

using Orleans.TestingHost;

namespace Tests;

public class HelloGrainTests
{
    [Fact]
    public async Task SaysHelloCorrectly()
    {
        var builder = new TestClusterBuilder();
        var cluster = builder.Build();
        cluster.Deploy();

        var hello = cluster.GrainFactory.GetGrain<IHelloGrain>(Guid.NewGuid());
        var greeting = await hello.SayHello("World");

        cluster.StopAllSilos();

        Assert.Equal("Hello, World!", greeting);
    }
}

Из-за накладных расходов на запуск кластерной системы в оперативной памяти может потребоваться создать TestCluster и повторно использовать его для нескольких тестовых вариантов. Например, это достигается с помощью класса или светильников коллекции xUnit.

Для совместного TestCluster использования нескольких тестовых вариантов сначала создайте тип светильника:

using Orleans.TestingHost;

public sealed class ClusterFixture : IDisposable
{
    public TestCluster Cluster { get; } = new TestClusterBuilder().Build();

    public ClusterFixture() => Cluster.Deploy();

    void IDisposable.Dispose() => Cluster.StopAllSilos();
}

Затем создайте светильник коллекции:

[CollectionDefinition(Name)]
public sealed class ClusterCollection : ICollectionFixture<ClusterFixture>
{
    public const string Name = nameof(ClusterCollection);
}

Теперь можно повторно использовать TestCluster в тестовых случаях:

using Orleans.TestingHost;

namespace Tests;

[Collection(ClusterCollection.Name)]
public class HelloGrainTestsWithFixture(ClusterFixture fixture)
{
    private readonly TestCluster _cluster = fixture.Cluster;

    [Fact]
    public async Task SaysHelloCorrectly()
    {
        var hello = _cluster.GrainFactory.GetGrain<IHelloGrain>(Guid.NewGuid());
        var greeting = await hello.SayHello("World");

        Assert.Equal("Hello, World!", greeting);
    }
}

После завершения всех тестов и остановки кластерных силосов в памяти, xUnit вызывает метод Dispose() типа ClusterFixture. TestCluster также имеет конструктор, принимаюющий TestClusterOptions , что можно использовать для настройки силосов в кластере.

Если вы используете внедрение зависимостей (Dependency Injection) в Silo, чтобы сделать службы доступными для Grains, вы также можете использовать этот шаблон:

using Microsoft.Extensions.DependencyInjection;
using Orleans.TestingHost;

namespace Tests;

public sealed class ClusterFixtureWithConfig : IDisposable
{
    public TestCluster Cluster { get; } = new TestClusterBuilder()
        .AddSiloBuilderConfigurator<TestSiloConfigurations>()
        .Build();

    public ClusterFixtureWithConfig() => Cluster.Deploy();

    void IDisposable.Dispose() => Cluster.StopAllSilos();
}

file sealed class TestSiloConfigurations : ISiloConfigurator
{
    public void Configure(ISiloBuilder siloBuilder)
    {
        // TODO: Call required service registrations here.
        // siloBuilder.Services.AddSingleton<T, Impl>(/* ... */);
    }
}

Использование макетов

Orleans также позволяет издеваться над многими частями системы. Для многих сценариев это самый простой способ модульного тестирования грейнов. Этот подход имеет свои ограничения (например, в отношении планирования повторного входа и сериализации) и может потребовать от "зерен" включения кода, используемого только в ваших модульных тестах. Orleans TestKit предоставляет альтернативный подход, который позволяет обойти многие из этих ограничений.

Например, представьте, что тестируемое зерно взаимодействует с другими зернами. Чтобы издеваться над другими зернами, вам также нужно издеваться GrainFactory над членом зерна под тестом. По умолчанию GrainFactory является обычным protected свойством, но большинство фреймворков для создания макетов требуют, чтобы свойства были public и virtual, чтобы включить макетирование. Таким образом, первый шаг состоит в том, чтобы сделать GrainFactory оба public и virtual:

public new virtual IGrainFactory GrainFactory
{
    get => base.GrainFactory;
}

Теперь вы можете создать грейн за пределами Orleans среды выполнения и использовать тестирование с помощью макетов для управления поведением GrainFactory:

using Xunit;
using Moq;

namespace Tests;

public class WorkerGrainTests
{
    [Fact]
    public async Task RecordsMessageInJournal()
    {
        var data = "Hello, World";
        var journal = new Mock<IJournalGrain>();
        var worker = new Mock<WorkerGrain>();
        worker
            .Setup(x => x.GrainFactory.GetGrain<IJournalGrain>(It.IsAny<Guid>()))
            .Returns(journal.Object);

        await worker.DoWork(data)

        journal.Verify(x => x.Record(data), Times.Once());
    }
}

Здесь создайте зерно для тестирования с помощью WorkerGrain и Moq. Это позволяет переопределить поведение GrainFactory, чтобы он возвращал имитированный IJournalGrain. Затем можно убедиться, что WorkerGrain взаимодействует с IJournalGrain должным образом.