Dela via


Enhetstestning med Orleans

Den här självstudien visar hur du enhetstestar dina korn för att se till att de fungerar korrekt. Det finns två huvudsakliga sätt att enhetstesta dina korn, och vilken metod du väljer beror på vilken typ av funktionalitet du testar. Använd Microsoft.Orleans.TestingHost NuGet-paketet för att skapa testsilor för dina grains, eller använd ett mock-ramverk som Moq för att mocka delar av Orleans-runtime som ditt grain interagerar med.

InProcessTestCluster är den rekommenderade testinfrastrukturen för Orleans. Det tillhandahåller ett effektiviserat, delegerat baserat API för att konfigurera testkluster, vilket gör det enklare att dela tjänster mellan dina tester och klustret.

Viktiga fördelar

Den främsta fördelen med InProcessTestCluster över TestCluster är ergonomi:

  • Delegerad konfiguration: Konfigurera silos och klienter med inline-delegater istället för separata konfigurationsklasser
  • Delade tjänstinstanser: Du kan enkelt dela mocktjänster, testdubblar och andra instanser mellan din testkod och värdar i silostrukturen.
  • Mindre mallkod: Du behöver inte skapa separata ISiloConfigurator eller IClientConfigurator klasser
  • Enklare beroendeinmatning: Registrera tjänster direkt i builder fluent-API:et

Både InProcessTestCluster och TestCluster använder samma underliggande silovärd i processen som standard, så minnesanvändning och starttid är likvärdiga. API:et TestCluster är utformat för att även stödja scenarier med flera processer (för produktionsliknande simulering), som kräver den klassbaserade konfigurationsmetoden, men som standard körs den i processen precis som InProcessTestCluster.

Grundläggande användning

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

Konfigurera testklustret

Använd InProcessTestClusterBuilder för att konfigurera silor, klienter och tjänster:

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

Option Typ Förinställning Description
ClusterId string Automatiskt genererad Klusteridentifierare.
ServiceId string Automatiskt genererad Tjänstidentifierare.
InitialSilosCount int 1 Antal silor som ska startas inledningsvis.
InitializeClientOnDeploy bool true Om klienten ska initieras automatiskt vid distribution.
ConfigureFileLogging bool true Aktivera filloggning för felsökning.
UseRealEnvironmentStatistics bool false Använd verklig minnes-/CPU-statistik i stället för simulerade värden.
GatewayPerSilo bool true Om varje silo tillhandahåller en gateway för klientanslutningar.

Dela ett testkluster mellan tester

För att förbättra testprestandan delar du ett enda kluster i flera testfall med hjälp av xUnit-armaturer:

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

Lägga till och ta bort silor under tester

Stöder InProcessTestCluster dynamisk silohantering för testning av klusterbeteende:

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

Använd TestCluster

Den TestCluster använder en klassbaserad konfigurationsmetod som kräver implementering av ISiloConfigurator och IClientConfigurator gränssnitt. Den här designen stöder scenarier för testning av flera processer där silor körs i separata processer, vilket är användbart för produktionsliknande simuleringstestning. Men som standard TestCluster körs även i processen med motsvarande prestanda till InProcessTestCluster.

Välj TestCluster över InProcessTestCluster när:

  • Du behöver multiprocesstestning för produktionssimulering
  • Du har befintliga tester med hjälp av API:et TestCluster
  • Du behöver kompatibilitet med Orleans 7.x eller 8.x

För nya tester InProcessTestCluster rekommenderas på grund av den enklare delegerade konfigurationen.

Microsoft.Orleans.TestingHost NuGet-paketet innehåller TestCluster, som du kan använda för att skapa ett minnesinternt kluster (bestående av två silor som standard) för testning av korn.

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

På grund av kostnaden för att starta ett minnesinternt kluster kanske du vill skapa ett TestCluster och återanvända det bland flera testfall. Du kan till exempel uppnå detta med hjälp av xUnits klass- eller samlingsfixturer.

Om du vill dela en TestCluster mellan flera testfall skapar du först en typ av fixtur:

using Orleans.TestingHost;

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

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

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

Skapa sedan en samlingsfixtur:

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

Nu kan du återanvända en TestCluster i dina testfall:

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

När alla tester har slutförts och de minnesinterna klusters silor stoppas anropar xUnit Dispose() metod från ClusterFixture typen. TestCluster har också en konstruktor som accepterar TestClusterOptions att du kan använda för att konfigurera silor i klustret.

Om du använder Dependency Injection i din Silo för att göra tjänster tillgängliga för Grains kan du också använda det här mönstret:

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

Använd mocks

Orleans gör det också möjligt att håna många delar av systemet. I många scenarier är det här det enklaste sättet att förena testkorn. Den här metoden har begränsningar (t.ex. kring schemaläggning av återaktivering och serialisering) och kan kräva att grains inkluderar kod som endast används av dina enhetstester. Orleans TestKit tillhandahåller en alternativ metod som kringgår många av dessa begränsningar.

Anta till exempel att kornet som du testar interagerar med andra korn. För att mocka de andra korn måste du också mocka GrainFactory medlemmen i kornet under testning. Som standard är GrainFactory en normal protected egenskap, men de flesta mockingramverk kräver att egenskaperna är public och virtual för att möjliggöra mockning. Så det första steget är att göra GrainFactory både public och virtual:

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

Nu kan du skapa din aktör utanför körningstiden Orleans och använda mocking för att styra beteendet hos 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());
    }
}

Skapa testobjektet WorkerGrain, med hjälp av Moq, här. Detta möjliggör att åsidosätta GrainFactory's beteende så att den returnerar en mockad IJournalGrain. Du kan sedan kontrollera att WorkerGrain interagerar med IJournalGrain som förväntat.