Partilhar via


Testes unitários com Orleans

Este tutorial mostra como testar os grãos para garantir que eles se comportam corretamente. Há duas maneiras principais de testar seus grãos por unidade, e o método escolhido depende do tipo de funcionalidade que você está testando. Use o pacote NuGet Microsoft.Orleans.TestingHost para criar silos de teste para os seus grãos, ou utiliza uma estrutura de simulação como Moq para simular partes do tempo de execução com o Orleans qual o seu grão interage.

InProcessTestCluster é a infraestrutura de teste recomendada para Orleans. Fornece uma API simplificada baseada em delegados para configurar clusters de teste, facilitando a partilha de serviços entre os seus testes e o cluster.

Principais vantagens

A principal vantagem do InProcessTestCluster over TestCluster é a ergonomia:

  • Configuração baseada em delegados: Configurar silos e clientes usando delegados inline em vez de classes de configuração separadas
  • Instâncias de serviço partilhado: Partilha facilmente serviços simulados, duplicações de teste e outras instâncias entre o teu código de teste e os anfitriões do silo
  • Menos padrão: Não é necessário criar classes separadas ISiloConfigurator ou IClientConfigurator
  • Injeção de dependências mais simples: Registar serviços diretamente na API fluente do builder

Ambos InProcessTestCluster e TestCluster usam, por defeito, o mesmo host silo subjacente em processo, o que significa que o uso de memória e o tempo de arranque são equivalentes. A TestCluster API foi concebida para suportar também cenários de múltiplos processos (para simulação semelhante à produção), o que requer a abordagem de configuração baseada em classes, mas por padrão corre em processo tal como InProcessTestCluster.

Utilização básica

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

Configurar o cluster de testes

Use InProcessTestClusterBuilder para configurar silos, clientes e serviços:

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

Opção Tipo Predefinido Description
ClusterId string Gerado automaticamente Identificador de cluster.
ServiceId string Gerado automaticamente Identificador do serviço.
InitialSilosCount int 1 Número de silos para começar inicialmente.
InitializeClientOnDeploy bool true Se deve inicializar automaticamente o cliente na implementação.
ConfigureFileLogging bool true Ativar o registo de ficheiros para depuração.
UseRealEnvironmentStatistics bool false Use estatísticas reais de memória/CPU em vez de valores simulados.
GatewayPerSilo bool true Se cada silo aloja um gateway para ligações ao cliente.

Partilhar um cluster de testes entre testes

Para melhorar o desempenho dos testes, partilhe um único cluster entre múltiplos casos de teste usando fixtures 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);
    }
}

Adicionar e remover silos durante os testes

A gestão dinâmica de silos pelo InProcessTestCluster suporta o teste do comportamento do cluster.

// 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();

Utilize o TestCluster

A TestCluster utiliza uma abordagem de configuração baseada em classes que requer a implementação das interfaces ISiloConfigurator e IClientConfigurator. Este design suporta cenários de teste multiprocesso onde silos correm em processos separados, o que é útil para testes de simulação semelhantes à produção. No entanto, por defeito TestCluster também corre em processo com desempenho equivalente a InProcessTestCluster.

Escolha TestCluster em vez de InProcessTestCluster quando:

  • Precisas de testes multiprocesso para simulação de produção
  • Tem testes existentes que utilizam a API TestCluster
  • Precisas de compatibilidade com Orleans 7.x ou 8.x

Para novos testes, InProcessTestCluster é recomendado devido à sua configuração mais simples baseada em delegados.

O Microsoft.Orleans.TestingHost pacote NuGet contém TestCluster, que você pode usar para criar um cluster na memória (composto por dois silos por padrão) para testar grãos.

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

Devido à sobrecarga de iniciar um cluster na memória, convém criar um TestCluster e reutilizá-lo entre vários casos de teste. Por exemplo, faça isso usando as fixtures de classe ou de coleção do xUnit.

Para partilhar um TestCluster entre vários casos de teste, primeiro crie um tipo de fixture:

using Orleans.TestingHost;

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

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

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

Em seguida, crie um acessório de coleção:

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

Agora você pode reutilizar um TestCluster em seus casos de teste:

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

Após todos os testes serem concluídos e os silos de cluster na memória pararem, xUnit chama o método Dispose() do tipo ClusterFixture. TestCluster também tem um construtor que aceita TestClusterOptions, que pode ser usado para configurar os silos no cluster.

Se utilizar a injeção de dependência no seu Silo para disponibilizar serviços aos Grãos, poderá usar este padrão também:

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>(/* ... */);
    }
}

Utilizar simulações

Orleans também permite imitar diversas partes do sistema. Para muitos cenários, essa é a maneira mais fácil de testar grãos por unidade. Essa abordagem tem limitações (por exemplo, em torno do agendamento de reentrância e serialização) e pode exigir que as 'grains' incluam código usado apenas pelos seus testes de unidade. O Orleans TestKit fornece uma abordagem alternativa que contorna muitas dessas limitações.

Por exemplo, imagine que o grão que você está testando interage com outros grãos. Para simular esses outros elementos, você também precisa simular o GrainFactory membro do elemento em teste. Por padrão, GrainFactory é uma propriedade normal protected, mas a maioria das estruturas de simulação requer que as propriedades sejam public e virtual para permitir a simulação. Assim, o primeiro passo é fazer GrainFactory tanto public como virtual:

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

Agora você pode criar seu grão fora do Orleans tempo de execução e usar mocking para controlar o comportamento de 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());
    }
}

Aqui, crie o grão em teste, WorkerGrain usando Moq. Isso permite substituir o comportamento do GrainFactory para que ele retorne um IJournalGrain simulado. Em seguida, pode verificar se WorkerGrain interage com IJournalGrain como esperado.