Partager via


Tests unitaires avec Orleans

Ce tutoriel montre comment tester unitairement vos grains pour s’assurer qu’ils se comportent correctement. Il existe deux méthodes principales pour tester vos grains et la méthode que vous choisissez dépend du type de fonctionnalité que vous testez. Utilisez le package NuGet Microsoft.Orleans.TestingHost pour créer des silos de test pour vos grains ou utilisez une infrastructure de simulation telle que Moq pour simuler des parties du Orleans runtime avec lesquelles votre grain interagit.

InProcessTestCluster est l'infrastructure de test recommandée pour Orleans. Il fournit une API simplifiée basée sur des délégués pour la configuration des clusters de test, ce qui facilite le partage de services entre vos tests et le cluster.

Avantages clés

L'avantage principal de InProcessTestCluster par rapport à TestCluster est l’ergonomie :

  • Configuration basée sur les délégués : configurer des silos et des clients à l’aide de délégués inline au lieu de classes de configuration distinctes
  • Instances de service partagé : partagez facilement des services fictifs, des doubles de test et d’autres instances entre votre code de test et les hôtes de silo
  • Moins de code standard : il n’est pas nécessaire de créer des classes ISiloConfigurator ou IClientConfigurator distinctes
  • Injection de dépendance plus simple : Inscrire des services directement dans l’API Fluent du générateur

Les deux InProcessTestCluster et TestCluster utilisent le même hôte de silo in-process sous-jacent par défaut. L’utilisation de la mémoire et le temps de démarrage sont donc équivalents. L’API TestCluster est conçue pour prendre également en charge des scénarios multiprocesseurs (pour la simulation de type production), ce qui nécessite l’approche de configuration basée sur la classe, mais par défaut, elle s’exécute dans le processus comme InProcessTestCluster.

Utilisation de base

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

Configurer le cluster de test

Permet InProcessTestClusterBuilder de configurer des silos, des clients et des services :

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

Choix Type Par défaut Descriptif
ClusterId string Généré automatiquement Identificateur de cluster.
ServiceId string Généré automatiquement Identificateur de service.
InitialSilosCount int 1 Nombre de silos à lancer au départ.
InitializeClientOnDeploy bool true Indique s’il faut initialiser automatiquement le client lors du déploiement.
ConfigureFileLogging bool true Activez la journalisation des fichiers pour le débogage.
UseRealEnvironmentStatistics bool false Utilisez des statistiques réelles de mémoire/processeur au lieu de valeurs simulées.
GatewayPerSilo bool true Indique si chaque silo héberge une passerelle pour les connexions clientes.

Partager un cluster de test entre les tests

Pour améliorer les performances des tests, partagez un seul cluster sur plusieurs cas de test à l’aide de appareils 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);
    }
}

Ajouter et supprimer des silos pendant les tests

La InProcessTestCluster gestion dynamique du silo prend en charge le test du comportement du 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();

Utiliser le TestCluster

Il utilise TestCluster une approche de configuration basée sur des classes qui nécessite l’implémentation des interfaces ISiloConfigurator et IClientConfigurator. Cette conception prend en charge les scénarios de test multiprocesseur dans lesquels les silos s’exécutent dans des processus distincts, ce qui est utile pour les tests de simulation de type production. Toutefois, par défaut TestCluster , il s’exécute également dans le processus avec des performances équivalentes à InProcessTestCluster.

Choisissez TestCluster plutôt que InProcessTestCluster quand :

  • Vous avez besoin de tests multiprocesseur pour la simulation de production
  • Vous avez des tests existants à l’aide de l’API TestCluster
  • Vous avez besoin de compatibilité avec Orleans 7.x ou 8.x

Pour les nouveaux tests, InProcessTestCluster est recommandé en raison de sa configuration fondée sur les délégués, qui est plus simple.

Le Microsoft.Orleans.TestingHost package NuGet contient TestCluster, que vous pouvez utiliser pour créer un cluster en mémoire (consistant en deux silos par défaut) pour tester les grains.

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

En raison de la surcharge de démarrage d’un cluster en mémoire, vous pouvez créer un TestCluster cluster en mémoire et le réutiliser parmi plusieurs cas de test. Par exemple, effectuez cette opération à l’aide des éléments de classe ou de collection de xUnit.

Pour partager une TestCluster entre plusieurs cas de test, commencez par créer un type 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();
}

Ensuite, créez un élément de collection :

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

Vous pouvez maintenant réutiliser un TestCluster dans vos cas de test :

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

Lorsque tous les tests se terminent et que les silos de cluster en mémoire s’arrêtent, xUnit appelle la méthode Dispose() du type ClusterFixture. TestCluster a également un constructeur acceptant TestClusterOptions que vous pouvez utiliser pour configurer les silos dans le cluster.

Si vous utilisez l'injection de dépendance dans votre Silo pour rendre les services disponibles aux Grains, vous pouvez également utiliser ce patron :

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

Utiliser des fictives

Orleans permet également de simuler de nombreuses parties du système. Pour de nombreux scénarios, il s’agit du moyen le plus simple de tester les grains de test unitaire. Cette approche présente les limitations (par exemple, concernant la planification de la réentrance et de la sérialisation) et peut nécessiter que les grains incluent du code utilisé uniquement par vos tests unitaires. Le Orleans TestKit offre une approche alternative qui évite bon nombre de ces limitations.

Par exemple, imaginez que le grain que vous testez interagit avec d’autres grains. Pour simuler ces autres grains, vous devez également simuler le GrainFactory membre du grain testé. Par défaut, GrainFactory est une propriété normale protected, mais la plupart des frameworks de simulation nécessitent que les propriétés soient public et virtual pour activer la simulation. Ainsi, la première étape consiste à faire GrainFactory à la fois public et virtual:

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

Vous pouvez maintenant créer votre grain en dehors du runtime et utiliser le mocking pour contrôler le comportement de Orleans:

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

Ici, créez le grain sous test, WorkerGrain, à l’aide de Moq. Cela permet de remplacer le comportement de l’objet GrainFactory afin qu’il retourne un objet simulé IJournalGrain. Vous pouvez ensuite vérifier que WorkerGrain interagit avec IJournalGrain comme prévu.