Freigeben über


Erweiterbarkeit von Microsoft.Testing.Platform

Microsoft.Testing.Platform besteht aus einem Testframework und einer beliebigen Anzahl von Erweiterungen , die im Prozess oder außerhalb des Prozesses ausgeführt werden können.

Wie im Abschnitt "Architektur " beschrieben, ist Microsoft.Testing.Platform für eine Vielzahl von Szenarien und Erweiterbarkeitspunkten konzipiert. Die primäre und wesentliche Erweiterung ist zweifellos das Testframework, das von Ihren Tests genutzt wird. Wenn Sie es nicht registrieren, tritt ein Startfehler auf. Das Testframework ist die einzige Erweiterung, die zum Ausführen einer Testsitzung zwingend notwendig ist.

Um Szenarien wie das Generieren von Testberichten, Code Coverage, das Wiederholen fehlgeschlagener Tests und andere mögliche Features zu unterstützen, die das Testframework selbst nicht bietet, müssen Sie einen Mechanismus bereitstellen, der die Verwendung anderer Erweiterungen in Verbindung mit dem Testframework zum Bereitstellen dieser Features ermöglicht.

Im Wesentlichen ist das Testframework die primäre Erweiterung, die Informationen zu den einzelnen Tests bereitstellt, aus denen die Testsammlung besteht. Es meldet, ob ein bestimmter Test erfolgreich war, fehlgeschlagen ist oder übersprungen wurde und kann zusätzliche Informationen zu jedem Test liefern, z. B. einen lesbaren Namen (als Anzeigename bezeichnet), die Quelldatei, die Zeile, in der der Test beginnt usw.

Der Erweiterungspunkt ermöglicht die Verwendung von Informationen, die vom Testframework bereitgestellt werden, um neue Artefakte zu generieren oder vorhandene Artefakte mit zusätzlichen Features zu verbessern. Eine häufig verwendete Erweiterung ist der TRX-Bericht-Generator, der TestNodeUpdateMessage abonniert und daraus eine XML-Berichtsdatei generiert.

Wie im Artikel zur Architektur erläutert, gibt es bestimmte Erweiterungspunkte, die nicht im selben Prozess wie das Testframework ausgeführt werden können. In der Regel ist dies aus den folgenden Gründen nicht möglich:

  • Die Umgebungsvariablen des Testhosts müssen geändert werden. Im Prozess des Testhosts selbst Änderungen vorzunehmen, wäre zu spät.
  • Der Prozess muss von außen überwacht werden, da der Testhost, auf dem Tests und Benutzercode ausgeführt werden, Fehler im Benutzercode aufweisen kann, die den Prozess selbst instabil machen und zu Hängenbleiben oder Abstürzen führen können. In solchen Fällen würde die Erweiterung zusammen mit dem Testhostprozess abstürzen oder hängenbleiben.

Aus diesen Gründen werden die Erweiterungspunkte in zwei Typen unterteilt:

  1. Prozessinterne Erweiterungen: Diese Erweiterungen arbeiten im selben Prozess wie das Testframework (In-Process).

    Sie können prozessinterne Erweiterungen über die ITestApplicationBuilder.TestHost-Eigenschaft registrieren:

    // ...
    var builder = await TestApplication.CreateBuilderAsync(args);
    builder.TestHost.AddXXX(...);
    // ...
    
  2. Prozessexterne Erweiterungen: Diese Erweiterungen arbeiten in einem separaten Prozess (Out-of-Process), sodass sie den Testhost überwachen können, ohne vom Testhost selbst beeinflusst zu werden.

    Sie können prozessexterne Erweiterungen über die ITestApplicationBuilder.TestHostControllers-Eigenschaft registrieren.

    var builder = await TestApplication.CreateBuilderAsync(args);
    builder.TestHostControllers.AddXXX(...);
    

    Es gibt auch einige Erweiterungen, die in beiden Szenarien funktionieren. Diese allgemeinen Erweiterungen verhalten sich in beiden Hosts identisch. Sie können diese Erweiterungen entweder über die TestHost- und TestHostController-Schnittstellen oder direkt auf der ITestApplicationBuilder-Ebene registrieren. Ein Beispiel für eine solche Erweiterung ist ICommandLineOptionsProvider.

Die IExtension-Schnittstelle

Die IExtension-Schnittstelle dient als grundlegende Schnittstelle für alle Erweiterungspunkte innerhalb der Testplattform. Sie wird in erster Linie verwendet, um beschreibende Informationen zur Erweiterung abzurufen und die Erweiterung selbst zu aktivieren oder zu deaktivieren.

Sehen Sie sich die folgende IExtension-Schnittstelle an:

public interface IExtension
{
    string Uid { get; }
    string Version { get; }
    string DisplayName { get; }
    string Description { get; }
    Task<bool> IsEnabledAsync();
}
  • Uid: Stellt den eindeutigen Bezeichner für die Erweiterung dar. Es ist wichtig, einen eindeutigen Wert für diese Zeichenfolge auszuwählen, um Konflikte mit anderen Erweiterungen zu vermeiden.

  • Version: Stellt die Version der Schnittstelle dar. Die Angabe der Version erfordert die Verwendung der semantischen Versionierung.

  • DisplayName: Eine benutzerfreundliche Darstellung des Namens (Anzeigename), die in Protokollen und beim Anfordern von Informationen mithilfe der Befehlszeilenoption --info angezeigt wird.

  • Description: Die Beschreibung der Erweiterung, die angezeigt wird, wenn Sie mithilfe der Befehlszeilenoption --info Informationen anfordern.

  • IsEnabledAsync(): Diese Methode wird beim Instanziieren der Erweiterung von der Testplattform aufgerufen. Wenn die Methode false zurückgibt, wird die Erweiterung ausgeschlossen. Diese Methode trifft Entscheidungen in der Regel basierend auf der Konfigurationsdatei oder einigen benutzerdefinierten Befehlszeilenoptionen. Benutzer geben häufig --customExtensionOption in der Befehlszeile an, um die Erweiterung selbst zu aktivieren.

Testframeworkerweiterung

Das Testframework ist die primäre Erweiterung, die der Testplattform die Fähigkeit verleiht, Tests zu ermitteln und auszuführen. Das Testframework ist dafür verantwortlich, die Ergebnisse der Tests an die Testplattform zu übermitteln. Das Testframework ist die einzige Erweiterung, die zum Ausführen einer Testsitzung zwingend notwendig ist.

Registrieren eines Testframeworks

In diesem Abschnitt wird erläutert, wie Sie das Testframework bei der Testplattform registrieren. Sie registrieren nur ein Testframework pro Testanwendungs-Generator mithilfe der API, wie in der TestApplication.RegisterTestFramework Dokumentation zur Microsoft.Testing.Platform-Architektur dargestellt.

Die Registrierungs-API wird wie folgt definiert:

ITestApplicationBuilder RegisterTestFramework(
    Func<IServiceProvider, ITestFrameworkCapabilities> capabilitiesFactory,
    Func<ITestFrameworkCapabilities, IServiceProvider, ITestFramework> adapterFactory);

Die RegisterTestFramework-API erwartet zwei Factorys:

  1. Func<IServiceProvider, ITestFrameworkCapabilities>: Dies ist ein Delegat, der ein Objekt akzeptiert, das die IServiceProvider-Schnittstelle implementiert, und ein Objekt zurückgibt, das die ITestFrameworkCapabilities-Schnittstelle implementiert. IServiceProvider bietet Zugriff auf Plattformdienste wie Konfigurationen, Protokollierungen (Logger) und Befehlszeilenargumente.

    Die ITestFrameworkCapabilities-Schnittstelle wird verwendet, um die vom Testframework unterstützten Funktionen für die Plattform und Erweiterungen anzukündigen. Durch die Implementierung und Unterstützung bestimmter Verhaltensweisen ermöglicht sie der Plattform und den Erweiterungen die korrekte Interaktion. Weitere Informationen zum Konzept von Funktionen finden Sie im entsprechenden Abschnitt.

  2. Func<ITestFrameworkCapabilities, IServiceProvider, ITestFramework>: Dies ist ein Delegat, der ein ITestFrameworkCapabilities-Objekt, bei dem es sich um die von Func<IServiceProvider, ITestFrameworkCapabilities> zurückgegebene Instanz handelt, und ein IServiceProvider-Objekt akzeptiert, um den Zugriff auf Plattformdienste bereitzustellen. Das erwartete Rückgabeobjekt ist ein Objekt, das die ITestFramework-Schnittstelle implementiert. ITestFramework dient als Ausführungs-Engine, die Tests ermittelt und ausführt und anschließend die Ergebnisse an die Testplattform übermittelt.

Die Notwendigkeit für die Plattform, die Erstellung von ITestFrameworkCapabilities und ITestFramework zu trennen, ist eine Optimierung, die verhindert, dass das Testframework erstellt wird, wenn die unterstützten Funktionen nicht zum Ausführen der aktuellen Testsitzung ausreichen.

Das folgende Benutzercodebeispiel zeigt eine Testframeworkregistrierung, für die ein leerer Funktionssatz zurückgegeben wird:

internal class TestingFrameworkCapabilities : ITestFrameworkCapabilities
{
    public IReadOnlyCollection<ITestFrameworkCapability> Capabilities => [];
}

internal class TestingFramework : ITestFramework
{
   public TestingFramework(ITestFrameworkCapabilities capabilities, IServiceProvider serviceProvider)
   {
       // ...
   }
   // Omitted for brevity...
}

public static class TestingFrameworkExtensions
{
    public static void AddTestingFramework(this ITestApplicationBuilder builder)
    {
        builder.RegisterTestFramework(
            _ => new TestingFrameworkCapabilities(),
            (capabilities, serviceProvider) => new TestingFramework(capabilities, serviceProvider));
    }
}

// ...

Sehen Sie sich nun den entsprechenden Einstiegspunkt in diesem Beispiel mit dem Registrierungscode an:

var testApplicationBuilder = await TestApplication.CreateBuilderAsync(args);
// Register the testing framework
testApplicationBuilder.AddTestingFramework();
using var testApplication = await testApplicationBuilder.BuildAsync();
return await testApplication.RunAsync();

Hinweis

Die Rückgabe eines leeren ITestFrameworkCapabilities-Objekts sollte die Ausführung der Testsitzung nicht verhindern. Alle Testframeworks sollten in der Lage sein, Tests zu ermitteln und auszuführen. Die Auswirkungen sollten sich auf Erweiterungen beschränken, die deaktiviert werden können, wenn ein bestimmtes Feature nicht im Testframework verfügbar ist.

Erstellen eines Testframeworks

Microsoft.Testing.Platform.Extensions.TestFramework.ITestFramework wird durch Erweiterungen implementiert, die ein Testframework bereitstellen:

public interface ITestFramework : IExtension
{
    Task<CreateTestSessionResult> CreateTestSessionAsync(CreateTestSessionContext context);
    Task ExecuteRequestAsync(ExecuteRequestContext context);
    Task<CloseTestSessionResult> CloseTestSessionAsync(CloseTestSessionContext context);
}

Die ITestFramework-Schnittstelle erbt von der IExtension-Schnittstelle, einer Schnittstelle, von der alle Erweiterungspunkte erben. IExtension wird verwendet, um den Namen und die Beschreibung der Erweiterung abzurufen. IExtension bietet auch die Möglichkeit, die Erweiterung im Setup über Task<bool> IsEnabledAsync() dynamisch zu aktivieren oder zu deaktivieren. Stellen Sie sicher, dass diese Methode true zurückgibt, wenn Sie keine speziellen Anforderungen haben.

Die CreateTestSessionAsync -Methode

Die CreateTestSessionAsync-Methode wird zu Beginn der Testsitzung aufgerufen und zum Initialisieren des Testframeworks verwendet. Die API akzeptiert ein CloseTestSessionContext-Objekt und gibt ein CloseTestSessionResult-Objekt zurück.

public sealed class CreateTestSessionContext : TestSessionContext
{
    public SessionUid SessionUid { get; }
    public ClientInfo Client { get; }
    public CancellationToken CancellationToken { get; }
}

public readonly struct SessionUid
{
    public string Value { get; }
}

public sealed class ClientInfo
{
    public string Id { get; }
    public string Version { get; }
}

Die SessionUid dient als eindeutiger Bezeichner für die aktuelle Testsitzung und stellt eine logische Verbindung mit den Ergebnissen der Sitzung her. Das ClientInfo-Objekt enthält Details zur Entität, die das Testframework aufruft. Mithilfe dieser Informationen kann das Testframework sein Verhalten ändern. Zum Zeitpunkt der Erstellung dieses Dokuments würde bei einer Konsolenausführung beispielsweise ein Clientname wie „testingplatform-console“ gemeldet werden. Das CancellationToken wird verwendet, um die Ausführung von CreateTestSessionAsync anzuhalten.

Das Rückgabeobjekt ist ein CloseTestSessionResult:

public sealed class CreateTestSessionResult
{
    public string? WarningMessage { get; set; }
    public string? ErrorMessage { get; set; }
    public bool IsSuccess { get; set; }
}

Mit der IsSuccess-Eigenschaft wird angegeben, ob die Sitzung erfolgreich erstellt wurde. Wenn sie false zurückgibt, wird die Testausführung angehalten.

Die CloseTestSessionAsync -Methode

Der CloseTestSessionAsync-Methode steht hinsichtlich der Funktionalität CreateTestSessionAsync gegenüber, wobei der einzige Unterschied die Objektnamen sind. Weitere Informationen finden Sie im Abschnitt CreateTestSessionAsync.

Die ExecuteRequestAsync -Methode

Die ExecuteRequestAsync-Methode akzeptiert ein Objekt vom Typ ExecuteRequestContext. Wie der Name schon sagt, enthält dieses Objekt die Details zu der Aktion, die das Testframework ausführen soll. Die ExecuteRequestContext-Definition lautet wie folgt:

public sealed class ExecuteRequestContext
{
    public IRequest Request { get; }
    public IMessageBus MessageBus { get; }
    public CancellationToken CancellationToken { get; }
    public void Complete();
}

IRequest: Dies ist die Basisschnittstelle für alle Anforderungstypen. Stellen Sie sich das Testframework als einen prozessinternen zustandsbehafteten Server vor, auf dem der Lebenszyklus stattfindet:

Sequenzdiagramm: Lebenszyklus des Testframeworks

Das obige Diagramm zeigt, dass die Testplattform nach dem Erstellen der Testframeworkinstanz drei Anforderungen ausgibt. Das Testframework verarbeitet diese Anforderungen und verwendet den IMessageBus-Dienst, der in der Anforderung selbst enthalten ist, um das Ergebnis für jede Anforderung zu übermitteln. Sobald eine bestimmte Anforderung verarbeitet wurde, ruft das Testframework die Complete()-Methode für die Anforderung auf, um anzugeben, dass sie von der Testplattform erfüllt wurde. Die Testplattform überwacht alle gesendeten Anforderungen. Nachdem alle Anforderungen erfüllt wurden, ruft sie CloseTestSessionAsync auf und verwirft die Instanz (sofern IDisposable/IAsyncDisposable implementiert ist). Da sich die Anforderungen und ihr Abschluss offensichtlich überlappen können, ist die parallele und asynchrone Ausführung von Anforderungen möglich.

Hinweis

Derzeit sendet die Testplattform keine überlappenden Anforderungen. Sie wartet auf den Abschluss einer Anforderung >>, bevor die nächste Anforderung gesendet wird. Dieses Verhalten kann sich in Zukunft jedoch ändern. Die Unterstützung für gleichzeitige Anforderungen wird durch das Funktionssystem bestimmt.

Die IRequest-Implementierung gibt die genaue Anforderung an, die erfüllt werden muss. Das Testframework identifiziert den Anforderungstyp und behandelt die Anforderung entsprechend. Wenn der Anforderungstyp nicht erkannt wird, sollte eine Ausnahme ausgelöst werden.

Ausführliche Informationen zu den verfügbaren Anforderungen finden Sie im Abschnitt IRequest.

IMessageBus: Dieser Dienst, der mit der Anforderung verknüpft ist, ermöglicht dem Testframework die asynchrone Veröffentlichung von Informationen über die laufende Anforderung auf der Testplattform. Der Nachrichtenbus dient als zentraler Hub für die Plattform und erleichtert die asynchrone Kommunikation zwischen allen Plattformkomponenten und Erweiterungen. Eine umfassende Liste der Informationen, die auf der Testplattform veröffentlicht werden können, finden Sie im Abschnitt IMessageBus.

CancellationToken: Dieses Token wird verwendet, um die Verarbeitung einer bestimmten Anforderung zu unterbrechen.

Complete(): Wie in der obigen Sequenz dargestellt, benachrichtigt die Complete-Methode die Plattform, dass die Anforderung erfolgreich verarbeitet wurde und alle relevanten Informationen an den IMessageBus übermittelt wurden.

Warnung

Wird Complete() nicht für die Anforderung aufgerufen, reagiert die Testanwendung nicht mehr.

Um das Testframework entsprechend Ihren Anforderungen oder den Anforderungen Ihrer Benutzer anzupassen, können Sie einen personalisierten Abschnitt in der Konfigurationsdatei oder benutzerdefinierte Befehlszeilenoptionen verwenden.

Behandeln von Anforderungen

Der folgende Abschnitt enthält eine detaillierte Beschreibung der verschiedenen Anforderungen, die ein Testframework empfangen und verarbeiten kann.

Bevor Sie mit dem nächsten Abschnitt fortfahren, müssen Sie das Konzept von IMessageBus verstehen, dem wesentlichen Dienst für die Übermittlung von Testausführungsinformationen an die Testplattform.

TestSessionContext

TestSessionContext ist eine freigegebene Eigenschaft, die für alle Anforderungen verwendet wird und Informationen zur laufenden Testsitzung bereitstellt:

public class TestSessionContext
{
    public SessionUid SessionUid { get; }
    public ClientInfo Client { get; }
}

public readonly struct SessionUid(string value)
{
    public string Value { get; }
}

public sealed class ClientInfo
{
    public string Id { get; }
    public string Version { get; }
}

Der TestSessionContext besteht aus der SessionUid, einem eindeutigen Bezeichner für die laufende Testsitzung, der zum Protokollieren und Korrelieren von Testsitzungsdaten verwendet wird. Er enthält auch den ClientInfo-Typ, der Details zum Initiator der Testsitzung bereitstellt. Das Testframework kann basierend auf der Identität des Initiators der Testsitzung verschiedene Routen auswählen oder unterschiedliche Informationen veröffentlichen.

Discover-Testausführungsanforderung

public class DiscoverTestExecutionRequest
{
    // Detailed in the custom section below
    public TestSessionContext Session { get; }

    // This is experimental and intended for future use, please disregard for now.
    public ITestExecutionFilter Filter { get; }
}

Die DiscoverTestExecutionRequest weist das Testframework an, die Tests zu ermitteln und diese Informationen an den IMessageBus zu übermitteln.

Wie im vorherigen Abschnitt beschrieben, ist DiscoveredTestNodeStateProperty die Eigenschaft für einen ermittelten Test. Im Folgenden sehen Sie zur Referenz einen generischen Codeschnipsel:

var testNode = new TestNode
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        DiscoveredTestNodeStateProperty.CachedInstance),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        discoverTestExecutionRequest.Session.SessionUid,
        testNode));

// ...

Ausführungstestanforderung starten

public class RunTestExecutionRequest
{
    // Detailed in the custom section below
    public TestSessionContext Session { get; }

    // This is experimental and intended for future use, please disregard for now.
    public ITestExecutionFilter Filter { get; }
}

Die RunTestExecutionRequest weist das Testframework an, die Tests auszuführen und diese Informationen an den IMessageBus zu übermitteln.

Im Folgenden sehen Sie zur Referenz einen generischen Codeschnipsel:

var skippedTestNode = new TestNode()
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        SkippedTestNodeStateProperty.CachedInstance),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        runTestExecutionRequest.Session.SessionUid,
        skippedTestNode));

// ...

var successfulTestNode = new TestNode()
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        PassedTestNodeStateProperty.CachedInstance),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        runTestExecutionRequest.Session.SessionUid,
        successfulTestNode));

// ...

var assertionFailedTestNode = new TestNode()
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        new FailedTestNodeStateProperty(assertionException)),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        runTestExecutionRequest.Session.SessionUid,
        assertionFailedTestNode));

// ...

var failedTestNode = new TestNode()
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        new ErrorTestNodeStateProperty(ex.InnerException!)),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        runTestExecutionRequest.Session.SessionUid,
        failedTestNode));

TestNodeUpdateMessage-Daten

Wie im Abschnitt IMessageBus erwähnt, müssen Sie vor der Verwendung des Nachrichtenbusses den Datentyp angeben, den Sie bereitstellen möchten. Die Testplattform hat einen bekannten Typ TestNodeUpdateMessage definiert, der das Konzept der Testaktualisierungsinformationen darstellt.

In diesem Teil des Dokuments wird erläutert, wie Sie diese Nutzlastdaten verwenden. Sehen wir uns die Oberfläche genauer an:

public sealed class TestNodeUpdateMessage(
    SessionUid sessionUid,
    TestNode testNode,
    TestNodeUid? parentTestNodeUid = null)
{
    public TestNode TestNode { get; }
    public TestNodeUid? ParentTestNodeUid { get; }
}

public class TestNode
{
    public required TestNodeUid Uid { get; init; }
    public required string DisplayName { get; init; }
    public PropertyBag Properties { get; init; } = new();
}

public sealed class TestNodeUid(string value)

public sealed partial class PropertyBag
{
    public PropertyBag();
    public PropertyBag(params IProperty[] properties);
    public PropertyBag(IEnumerable<IProperty> properties);
    public int Count { get; }
    public void Add(IProperty property);
    public bool Any<TProperty>();
    public TProperty? SingleOrDefault<TProperty>();
    public TProperty Single<TProperty>();
    public TProperty[] OfType<TProperty>();
    public IEnumerable<IProperty> AsEnumerable();
    public IEnumerator<IProperty> GetEnumerator();
    ...
}

public interface IProperty
{
}
  • TestNodeUpdateMessage: Die TestNodeUpdateMessage besteht aus zwei Eigenschaften: einem TestNode und einer ParentTestNodeUid. Die ParentTestNodeUid-Eigenschaft gibt an, dass ein Test möglicherweise über einen übergeordneten Test verfügt, wodurch das Konzept einer Teststruktur eingeführt wird, in der TestNode-Instanzen in Bezug zueinander angeordnet werden können. Diese Struktur ermöglicht basierend auf der Strukturbeziehung zwischen den Knoten zukünftige Verbesserungen und Features. Wenn Ihr Testframework keine Teststruktur erfordert, können Sie die Eigenschaft einfach auf NULL festlegen, sodass Sie eine einfache flache Liste von TestNode-Instanzen erhalten.

  • TestNode: Der TestNode (Testknoten) besteht aus drei Eigenschaften, von denen eine die Uid vom Typ TestNodeUid ist. Diese Uid dient als EINDEUTIGE STABILE ID für den Knoten. Der Begriff EINDEUTIGE STABILE ID impliziert, dass derselbe TestNode bei unterschiedlichen Ausführungen und auf verschiedenen Betriebssystemen eine IDENTISCHEUid beibehalten sollte. Die TestNodeUid ist eine beliebige nicht transparente Zeichenfolge, die von der Testplattform unverändert akzeptiert wird.

Von Bedeutung

Die Stabilität und Eindeutigkeit der ID sind in der Testdomäne von entscheidender Bedeutung. Sie ermöglichen die genaue Ausrichtung der Ausführung auf einen einzelnen Test und die Verwendung der ID als persistenten Bezeichner für einen Test, wodurch leistungsstarke Erweiterungen und Features ermöglicht werden.

Die zweite Eigenschaft ist DisplayName, der lesbare Name bzw. Anzeigename für den Test. Dieser Name wird beispielsweise angezeigt, wenn Sie die Befehlszeile --list-tests ausführen.

Das dritte Attribut ist Properties, ein PropertyBag-Typ. Wie im Code gezeigt, ist dies ein spezieller Eigenschaftenbehälter, der generische Eigenschaften in Bezug auf die TestNodeUpdateMessage enthält. Dies bedeutet, dass Sie eine beliebige Eigenschaft an den Knoten anfügen können, der die Platzhalterschnittstelle IProperty implementiert.

Die Testplattform identifiziert bestimmte Eigenschaften, die TestNode.Properties hinzugefügt wurden, um zu bestimmen, ob ein Test erfolgreich war, fehlgeschlagen ist oder übersprungen wurde.

Sie finden die aktuelle Liste der verfügbaren Eigenschaften mit der relativen Beschreibung im Abschnitt TestNodeUpdateMessage.TestNode.

Auf den PropertyBag-Typ kann in der Regel in jeder IData-Schnittstelle zugegriffen werden. Er wird verwendet, um verschiedene Eigenschaften zu speichern, die von der Plattform und den Erweiterungen abgefragt werden können. Dieser Mechanismus ermöglicht es, die Plattform mit neuen Informationen zu verbessern, ohne Breaking Changes einzuführen. Wenn eine Komponente die Eigenschaft erkennt, kann sie sie abfragen. Andernfalls ignoriert sie die Eigenschaft.

Abschließend wird in diesem Abschnitt verdeutlicht, dass Ihre Testframeworkimplementierung wie im folgenden Beispiel gezeigt die IDataProducer-Schnittstelle implementieren muss, die TestNodeUpdateMessages erzeugt:

internal sealed class TestingFramework
    : ITestFramework, IDataProducer
{
   // ...

   public Type[] DataTypesProduced =>
   [
       typeof(TestNodeUpdateMessage)
   ];

   // ...
}

Wenn Ihr Testadapter die Veröffentlichung von Dateien während der Ausführung erfordert, finden Sie die erkannten Eigenschaften in dieser Quelldatei: https://github.com/microsoft/testfx/blob/main/src/Platform/Microsoft.Testing.Platform/Messages/FileArtifacts.cs. Wie Sie sehen, können Sie Dateiressourcen auf allgemeine Weise bereitstellen oder sie einem bestimmten TestNode zuordnen. Wenn Sie vorhaben, ein SessionFileArtifact zu pushen, müssen Sie daran denken, dieses wie unten dargestellt vorab gegenüber der Plattform zu deklarieren:

internal sealed class TestingFramework
    : ITestFramework, IDataProducer
{
   // ...

   public Type[] DataTypesProduced =>
   [
       typeof(TestNodeUpdateMessage),
       typeof(SessionFileArtifact)
   ];

   // ...
}

Bekannte Eigenschaften

Wie im Anfrageabschnitt beschrieben, identifiziert die Testplattform bestimmte Eigenschaften, die dem TestNodeUpdateMessage hinzugefügt werden, um den Status eines TestNode zu bestimmen (z. B. erfolgreich, fehlgeschlagen, übersprungen usw.). Auf diese Weise kann die Runtime eine genaue Liste der fehlgeschlagenen Tests mit den entsprechenden Informationen in der Konsole anzeigen und den entsprechenden Exitcode für den Testprozess festlegen.

In diesem Abschnitt erläutern wir die verschiedenen bekannten IProperty-Optionen und ihre jeweiligen Auswirkungen.

Eine umfassende Liste bekannter Eigenschaften finden Sie unter TestNodeProperties.cs. Wenn Sie feststellen, dass eine Eigenschaftsbeschreibung fehlt, geben Sie bitte ein Problem an.

Diese Eigenschaften lassen sich in folgende Kategorien unterteilen:

  1. Allgemeine Informationen: Eigenschaften, die in jede Anforderung eingeschlossen werden können.
  2. Ermittlungsinformationen: Eigenschaften, die während einer Ermittlungsanforderung DiscoverTestExecutionRequest bereitgestellt werden.
  3. Ausführungsinformationen: Eigenschaften, die während einer Testausführungsanforderung RunTestExecutionRequest bereitgestellt werden.

Bestimmte Eigenschaften sind erforderlich und andere optional. Die obligatorischen Eigenschaften sind erforderlich, um grundlegende Testfunktionen bereitzustellen, z. B. das Melden fehlgeschlagener Tests und die Angabe, ob die gesamte Testsitzung erfolgreich war oder nicht.

Optionale Eigenschaften verbessern hingegen die Testerfahrung, indem sie zusätzliche Informationen bereitstellen. Sie sind besonders nützlich in IDE-Szenarien (Integrated Development Environment, integrierte Entwicklungsumgebung) wie z. B. Visual Studio und Visual Studio Code, bei Konsolenausführungen oder bei der Unterstützung bestimmter Erweiterungen, die detailliertere Informationen erfordern, damit sie korrekt funktionieren. Diese optionalen Eigenschaften haben jedoch keine Auswirkung auf die Ausführung der Tests.

Hinweis

Erweiterungen müssen Warnungen generieren und Ausnahmen verwalten, wenn für ihre ordnungsgemäße Funktion bestimmte Informationen erforderlich sind. Wenn eine Erweiterung nicht über die erforderlichen Informationen verfügt, sollte sie keinen Fehler der Testausführung auslösen, sondern sich einfach deaktivieren.

Allgemeine Informationen
public record KeyValuePairStringProperty(
    string Key,
    string Value)
        : IProperty;

Die KeyValuePairStringProperty steht für allgemeine Daten in Form von Schlüssel-Wert-Paaren.

public record struct LinePosition(
    int Line,
    int Column);

public record struct LinePositionSpan(
    LinePosition Start,
    LinePosition End);

public abstract record FileLocationProperty(
    string FilePath,
    LinePositionSpan LineSpan)
        : IProperty;

public sealed record TestFileLocationProperty(
    string FilePath,
    LinePositionSpan LineSpan)
        : FileLocationProperty(FilePath, LineSpan);

TestFileLocationProperty wird verwendet, um die Position des Tests innerhalb der Quelldatei genau zu bestimmen. Dies ist besonders hilfreich, wenn der Initiator eine IDE wie Visual Studio oder Visual Studio Code ist.

public sealed record TestMethodIdentifierProperty(
    string AssemblyFullName,
    string Namespace,
    string TypeName,
    string MethodName,
    string[] ParameterTypeFullNames,
    string ReturnTypeFullName)

TestMethodIdentifierProperty ist ein eindeutiger Bezeichner für eine Testmethode, die dem ECMA-335-Standard entspricht.

Hinweis

Die zum Erstellen dieser Eigenschaft erforderlichen Daten können bequem mithilfe des .NET-Reflexionsfeatures mit Typen aus dem System.Reflection-Namespace abgerufen werden.

public sealed record TestMetadataProperty(
    string Key,
    string Value)

TestMetadataProperty wird verwendet, um die Eigenschaften oder Merkmale eines TestNode zu übermitteln.

Erkennungsinformationen
public sealed record DiscoveredTestNodeStateProperty(
    string? Explanation = null)
{
    public static DiscoveredTestNodeStateProperty CachedInstance { get; }
}

Die DiscoveredTestNodeStateProperty gibt an, dass dieser Testknoten (TestNode) erkannt wurde. Sie wird verwendet, wenn eine DiscoverTestExecutionRequest an das Testframework gesendet wird. Notieren Sie sich den praktischen zwischengespeicherten Wert, den die CachedInstance-Eigenschaft bereitstellt. Diese Eigenschaft ist erforderlich.

Ausführungsinformationen
public sealed record InProgressTestNodeStateProperty(
    string? Explanation = null)
{
    public static InProgressTestNodeStateProperty CachedInstance { get; }
}

Die InProgressTestNodeStateProperty informiert die Testplattform, dass der TestNode zur Ausführung geplant wurde und derzeit ausgeführt wird. Notieren Sie sich den praktischen zwischengespeicherten Wert, den die CachedInstance-Eigenschaft bereitstellt.

public readonly record struct TimingInfo(
    DateTimeOffset StartTime,
    DateTimeOffset EndTime,
    TimeSpan Duration);

public sealed record StepTimingInfo(
    string Id,
    string Description,
    TimingInfo Timing);

public sealed record TimingProperty : IProperty
{
    public TimingProperty(TimingInfo globalTiming)
        : this(globalTiming, [])
    {
    }

    public TimingProperty(
        TimingInfo globalTiming,
        StepTimingInfo[] stepTimings)
    {
        GlobalTiming = globalTiming;
        StepTimings = stepTimings;
    }

    public TimingInfo GlobalTiming { get; }

    public StepTimingInfo[] StepTimings { get; }
}

Die TimingProperty wird verwendet, um Details zur zeitlichen Steuerung der TestNode-Ausführung weiterzuleiten. Sie ermöglicht auch die zeitliche Steuerung einzelner Ausführungsschritte über StepTimingInfo. Dies ist besonders nützlich, wenn Ihr Testkonzept in mehrere Phasen unterteilt ist (z. B. Initialisierung, Ausführung und Bereinigung).

Nur eine der folgenden Eigenschaften ist erforderlich pro TestNode. Sie kommuniziert das Ergebnis des TestNode an die Testplattform.

public sealed record PassedTestNodeStateProperty(
    string? Explanation = null)
        : TestNodeStateProperty(Explanation)
{
    public static PassedTestNodeStateProperty CachedInstance
        { get; } = new PassedTestNodeStateProperty();
}

PassedTestNodeStateProperty informiert die Testplattform, dass dieser TestNode erfolgreich war. Notieren Sie sich den praktischen zwischengespeicherten Wert, den die CachedInstance-Eigenschaft bereitstellt.

public sealed record SkippedTestNodeStateProperty(
    string? Explanation = null)
        : TestNodeStateProperty(Explanation)
{
    public static SkippedTestNodeStateProperty CachedInstance
        { get; } =  new SkippedTestNodeStateProperty();
}

SkippedTestNodeStateProperty informiert die Testplattform, dass dieser TestNode übersprungen wurde. Notieren Sie sich den praktischen zwischengespeicherten Wert, den die CachedInstance-Eigenschaft bereitstellt.

public sealed record FailedTestNodeStateProperty : TestNodeStateProperty
{
    public FailedTestNodeStateProperty()
        : base(default(string))
    {
    }

    public FailedTestNodeStateProperty(string explanation)
        : base(explanation)
    {
    }

    public FailedTestNodeStateProperty(
        Exception exception,
        string? explanation = null)
        : base(explanation ?? exception.Message)
    {
        Exception = exception;
    }

    public Exception? Exception { get; }
}

FailedTestNodeStateProperty informiert die Testplattform, dass dieser TestNode nach einer Assertion fehlgeschlagen ist.

public sealed record ErrorTestNodeStateProperty : TestNodeStateProperty
{
    public ErrorTestNodeStateProperty()
        : base(default(string))
    {
    }

    public ErrorTestNodeStateProperty(string explanation)
        : base(explanation)
    {
    }

    public ErrorTestNodeStateProperty(
        Exception exception,
        string? explanation = null)
            : base(explanation ?? exception.Message)
    {
        Exception = exception;
    }

    public Exception? Exception { get; }
}

ErrorTestNodeStateProperty informiert die Testplattform, dass bei diesem TestNode ein Fehler aufgetreten ist. Dieser Fehler unterscheidet sich von der FailedTestNodeStateProperty, die für Assertionsfehler verwendet wird. Beispielsweise können Sie Probleme wie Testinitialisierungsfehler mit ErrorTestNodeStateProperty melden.

public sealed record TimeoutTestNodeStateProperty : TestNodeStateProperty
{
    public TimeoutTestNodeStateProperty()
        : base(default(string))
    {
    }

    public TimeoutTestNodeStateProperty(string explanation)
        : base(explanation)
    {
    }

    public TimeoutTestNodeStateProperty(
        Exception exception,
        string? explanation = null)
            : base(explanation ?? exception.Message)
    {
        Exception = exception;
    }

    public Exception? Exception { get; }

    public TimeSpan? Timeout { get; init; }
}

TimeoutTestNodeStateProperty informiert die Testplattform, dass dieser TestNode aufgrund einer Zeitüberschreitung (Timeout) fehlgeschlagen ist. Sie können das Timeout mithilfe der Timeout-Eigenschaft melden.

public sealed record CancelledTestNodeStateProperty : TestNodeStateProperty
{
    public CancelledTestNodeStateProperty()
        : base(default(string))
    {
    }

    public CancelledTestNodeStateProperty(string explanation)
        : base(explanation)
    {
    }

    public CancelledTestNodeStateProperty(
        Exception exception,
        string? explanation = null)
        : base(explanation ?? exception.Message)
    {
        Exception = exception;
    }

    public Exception? Exception { get; }
}

CancelledTestNodeStateProperty informiert die Testplattform, dass dieser TestNode aufgrund eines Abbruchs fehlgeschlagen ist.

Andere Erweiterungspunkte

Die Testplattform bietet zusätzliche Erweiterungspunkte, mit denen Sie das Verhalten der Plattform und des Testframeworks anpassen können. Diese Erweiterungspunkte sind optional und können verwendet werden, um die Testerfahrung zu verbessern.

ICommandLineOptionsProvider-Erweiterungen

Hinweis

Wenn Sie diese API erweitern, ist die benutzerdefinierte Erweiterung sowohl innerhalb als auch außerhalb des Testhostprozesses vorhanden.

Wie im Abschnitt zur Architektur erläutert, erstellen Sie im ersten Schritt den ITestApplicationBuilder, um das Testframework und die Erweiterungen zu registrieren.

var builder = await TestApplication.CreateBuilderAsync(args);

Die CreateBuilderAsync-Methode akzeptiert ein Array von Zeichenfolgen (string[]) namens args. Diese Argumente können verwendet werden, um Befehlszeilenoptionen an alle Komponenten der Testplattform zu übergeben (einschließlich integrierter Komponenten, Testframeworks und Erweiterungen), sodass ihr Verhalten angepasst werden kann.

In der Regel werden die in der Main(string[] args)-Standardmethode empfangenen Argumente übergeben. Wenn sich die Hostumgebung unterscheidet, kann jedoch eine beliebige Liste von Argumenten übergeben werden.

Argumente erfordern als Präfix einen doppelten Gedankenstrich --. Beispiel: --filter.

Wenn eine Komponente wie ein Testframework oder ein Erweiterungspunkt benutzerdefinierte Befehlszeilenoptionen bereitstellen soll, ist dies durch Implementieren der ICommandLineOptionsProvider-Schnittstelle möglich. Diese Implementierung kann dann wie folgt mit der ITestApplicationBuilder-Schnittstelle über die Registrierungsfactory der CommandLine-Eigenschaft registriert werden:

builder.CommandLine.AddProvider(
    static () => new CustomCommandLineOptions());

Im Beispiel ist CustomCommandLineOptions eine Implementierung der ICommandLineOptionsProvider-Schnittstelle. Diese Schnittstelle umfasst die folgenden Member und Datentypen:

public interface ICommandLineOptionsProvider : IExtension
{
    IReadOnlyCollection<CommandLineOption> GetCommandLineOptions();

    Task<ValidationResult> ValidateOptionArgumentsAsync(
        CommandLineOption commandOption,
        string[] arguments);

    Task<ValidationResult> ValidateCommandLineOptionsAsync(
        ICommandLineOptions commandLineOptions);
}

public sealed class CommandLineOption
{
    public string Name { get; }
    public string Description { get; }
    public ArgumentArity Arity { get; }
    public bool IsHidden { get; }

    // ...
}

public interface ICommandLineOptions
{
    bool IsOptionSet(string optionName);

    bool TryGetOptionArgumentList(
        string optionName,
        out string[]? arguments);
}

Wie Sie sehen, erweitert die ICommandLineOptionsProvider-Schnittstelle die IExtension-Schnittstelle. Daher können Sie diese Erweiterung wie jede andere mithilfe der IExtension.IsEnabledAsync-API aktivieren oder deaktivieren.

Die Ausführungsreihenfolge von ICommandLineOptionsProvider lautet wie folgt:

Diagramm: Ausführungsreihenfolge der ICommandLineOptionsProvider-Schnittstelle

Sehen wir uns die APIs und ihre Bedeutung genauer an:

ICommandLineOptionsProvider.GetCommandLineOptions(): Diese Methode wird verwendet, um alle Optionen abzurufen, die die Komponente bietet. Für jede CommandLineOption (Befehlszeilenoption) müssen die folgenden Eigenschaften angegeben werden:

string name: Dies ist der Name der Option (dargestellt ohne Gedankenstrich). Die Option filter wird von Benutzern beispielsweise als --filter verwendet.

string description: Dies ist eine Beschreibung der Option. Sie wird angezeigt, wenn Benutzer --help als Argument an den Anwendungs-Generator übergeben.

ArgumentArity arity: Die Stelligkeit oder Arität (Anzahl von Argumenten) einer Option ist die Anzahl von Werten, die übergeben werden können, wenn diese Option oder dieser Befehl angegeben ist. Folgende Stelligkeiten sind derzeit verfügbar:

  • Zero: Stellt eine Stelligkeit der Argumente von Null dar.
  • ZeroOrOne: Stellt eine Stelligkeit der Argumente von Null oder 1 dar.
  • ZeroOrMore: Stellt eine Stelligkeit der Argumente von Null oder mehr dar.
  • OneOrMore: Stellt eine Stelligkeit der Argumente von 1 oder mehr dar.
  • ExactlyOne: Stellt eine Stelligkeit der Argumente von genau 1 dar.

Beispiele finden Sie in der Tabelle zur Argumentarität von System.CommandLine.

bool isHidden: Diese Eigenschaft gibt an, dass die Option zur Verwendung verfügbar ist, aber nicht in der Beschreibung angezeigt wird, wenn --help aufgerufen wird.

ICommandLineOptionsProvider.ValidateOptionArgumentsAsync: Diese Methode wird verwendet, um das vom Benutzer bereitgestellte Argument zu überprüfen.

Wenn Sie beispielsweise einen Parameter namens --dop haben, der den Grad der Parallelität für das benutzerdefinierte Testframework darstellt, kann ein Benutzer --dop 0 eingeben. In diesem Szenario wäre der Wert 0 ungültig, da ein Parallelitätsgrad von mindestens 1 erwartet wird. Mithilfe von ValidateOptionArgumentsAsync können Sie eine Vorabüberprüfung durchführen und bei Bedarf eine Fehlermeldung zurückgeben.

Eine mögliche Implementierung für das obige Beispiel könnte wie folgt aussehen:

public Task<ValidationResult> ValidateOptionArgumentsAsync(
    CommandLineOption commandOption,
    string[] arguments)
{
    if (commandOption.Name == "dop")
    {
        if (!int.TryParse(arguments[0], out int dopValue) || dopValue <= 0)
        {
            return ValidationResult.InvalidTask("--dop must be a positive integer");
        }
    }

    return ValidationResult.ValidTask;
}

ICommandLineOptionsProvider.ValidateCommandLineOptionsAsync: Diese Methode wird zuletzt aufgerufen und ermöglicht die Durchführung einer globalen Kohärenzprüfung.

Angenommen, unser Testframework bietet eine Funktion, mit der ein Testergebnisbericht generiert und in einer Datei gespeichert werden kann. Auf dieses Feature wird mithilfe der Option --generatereport zugegriffen, und der Dateiname wird mit --reportfilename myfile.rep angegeben. Wenn ein Benutzer in diesem Szenario die Option --generatereport, aber keinen Dateinamen angibt, sollte die Überprüfung fehlschlagen, da der Bericht ohne einen Dateinamen nicht generiert werden kann. Eine mögliche Implementierung für das obige Beispiel könnte wie folgt aussehen:

public Task<ValidationResult> ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions)
{
    bool generateReportEnabled = commandLineOptions.IsOptionSet(GenerateReportOption);
    bool reportFileName = commandLineOptions.TryGetOptionArgumentList(ReportFilenameOption, out string[]? _);

    return (generateReportEnabled || reportFileName) && !(generateReportEnabled && reportFileName)
        ? ValidationResult.InvalidTask("Both `--generatereport` and `--reportfilename` need to be provided simultaneously.")
        : ValidationResult.ValidTask;
}

Beachten Sie, dass die ValidateCommandLineOptionsAsync-Methode den ICommandLineOptions-Dienst bereitstellt, der zum Abrufen der Argumentinformationen verwendet wird, die von der Plattform selbst analysiert werden.

ITestSessionLifetimeHandler-Erweiterungen

ITestSessionLifeTimeHandler ist eine prozessinterne Erweiterung, die die Ausführung von Code vor und nach der Testsitzung ermöglicht.

Verwenden Sie die folgende API, um eine benutzerdefinierte ITestSessionLifeTimeHandler-Schnittstelle zu registrieren:

var builder = await TestApplication.CreateBuilderAsync(args);

// ...

builder.TestHost.AddTestSessionLifetimeHandle(
    static serviceProvider => new CustomTestSessionLifeTimeHandler());

Die Factory nutzt den IServiceProvider, um Zugriff auf die Sammlung von Diensten zu erhalten, die die Testplattform bietet.

Von Bedeutung

Die Registrierungsreihenfolge ist wichtig, da die APIs in der Reihenfolge aufgerufen werden, in der sie registriert wurden.

Die ITestSessionLifeTimeHandler-Schnittstelle umfasst die folgenden Methoden:

public interface ITestSessionLifetimeHandler : ITestHostExtension
{
    Task OnTestSessionStartingAsync(
        SessionUid sessionUid,
        CancellationToken cancellationToken);

    Task OnTestSessionFinishingAsync(
        SessionUid sessionUid,
        CancellationToken cancellationToken);
}

public readonly struct SessionUid(string value)
{
    public string Value { get; } = value;
}

public interface ITestHostExtension : IExtension
{
}

ITestSessionLifetimeHandler ist ein Typ von ITestHostExtension, der als Basis für alle Testhosterweiterungen dient. Wie alle anderen Erweiterungspunkte erbt er ebenfalls von IExtension. Daher können Sie diese Erweiterung wie jede andere mithilfe der IExtension.IsEnabledAsync-API aktivieren oder deaktivieren.

Beachten Sie für diese API die folgenden Details:

OnTestSessionStartingAsync: Diese Methode wird vor Beginn der Testsitzung aufgerufen und empfängt das SessionUid-Objekt, das einen nicht transparenten Bezeichner für die aktuelle Testsitzung bereitstellt.

OnTestSessionFinishingAsync: Diese Methode wird nach Abschluss der Testsitzung aufgerufen, um sicherzustellen, dass das Testframework die Ausführung aller Tests abgeschlossen und alle relevanten Daten an die Plattform gemeldet hat. In dieser Methode verwendet die Erweiterung in der Regel den IMessageBus, um benutzerdefinierte Ressourcen oder Daten an den freigegebenen Plattformbus zu übermitteln. Diese Methode kann auch beliebigen prozessexternen Erweiterungen den Abschluss der Testsitzung signalisieren.

Beide APIs akzeptieren zudem ein CancellationToken, das von der Erweiterung berücksichtigt werden muss.

Wenn Ihre Erweiterung eine intensive Initialisierung erfordert und Sie das async/await-Muster verwenden müssen, finden Sie unter Async extension initialization and cleanup weitere Informationen. Informationen zum Freigeben des Status zwischen Erweiterungspunkten finden Sie im Abschnitt CompositeExtensionFactory<T>.

ITestApplicationLifecycleCallbacks-Erweiterungen

ITestApplicationLifecycleCallbacks ist eine prozessinterne Erweiterung, die die Ausführung von Code vor allen anderen Funktionen ermöglicht. Sie bietet sozusagen Zugriff auf die erste Zeile der hypothetischen main-Methode des Testhosts.

Verwenden Sie die folgende API, um eine benutzerdefinierte ITestApplicationLifecycleCallbacks-Erweiterung zu registrieren:

var builder = await TestApplication.CreateBuilderAsync(args);

// ...

builder.TestHost.AddTestApplicationLifecycleCallbacks(
    static serviceProvider
    => new CustomTestApplicationLifecycleCallbacks());

Die Factory nutzt den IServiceProvider, um Zugriff auf die Sammlung von Diensten zu erhalten, die die Testplattform bietet.

Von Bedeutung

Die Registrierungsreihenfolge ist wichtig, da die APIs in der Reihenfolge aufgerufen werden, in der sie registriert wurden.

Die ITestApplicationLifecycleCallbacks-Schnittstelle umfasst die folgenden Methoden:

public interface ITestApplicationLifecycleCallbacks : ITestHostExtension
{
    Task BeforeRunAsync(CancellationToken cancellationToken);

    Task AfterRunAsync(
        int exitCode,
        CancellationToken cancellation);
}

public interface ITestHostExtension : IExtension
{
}

ITestApplicationLifecycleCallbacks ist ein Typ von ITestHostExtension, der als Basis für alle Testhosterweiterungen dient. Wie alle anderen Erweiterungspunkte erbt er ebenfalls von IExtension. Daher können Sie diese Erweiterung wie jede andere mithilfe der IExtension.IsEnabledAsync-API aktivieren oder deaktivieren.

BeforeRunAsync: Diese Methode dient als anfänglicher Kontaktpunkt für den Testhost und ist für eine prozessinterne Erweiterung die erste Möglichkeit zum Ausführen eines Features. Sie wird in der Regel verwendet, um eine Verbindung mit entsprechenden prozessexternen Erweiterungen herzustellen, wenn ein Feature für die Ausführung in beiden Umgebungen konzipiert ist.

Das integrierte Feature zum Erstellen eines Speicherabbilds bei Nichtreagieren besteht beispielsweise aus prozessinternen und prozessexternen Erweiterungen, und diese Methode wird verwendet, um Informationen mit der prozessexternen Komponente der Erweiterung auszutauschen.

AfterRunAsync: Diese Methode ist der letzte Aufruf vor dem Beenden der int ITestApplication.RunAsync()-Methode und stellt den exit code bereit. Sie sollte ausschließlich verwendet werden, um Bereinigungsaufgaben durchzuführen und alle entsprechenden prozessexternen Erweiterungen darüber zu benachrichtigen, dass der Testhost beendet wird.

Beide APIs akzeptieren zudem ein CancellationToken, das von der Erweiterung berücksichtigt werden muss.

IDataConsumer-Erweiterungen

IDataConsumer ist eine prozessinterne Erweiterung, die IData-Informationen abonnieren und empfangen kann, die vom Testframework und den Erweiterungen an den IMessageBus gepusht werden.

Dieser Erweiterungspunkt ist entscheidend, da er Entwicklern das Sammeln und Verarbeiten aller Informationen ermöglicht, die während einer Testsitzung generiert werden.

Verwenden Sie die folgende API, um eine benutzerdefinierte IDataConsumer-Erweiterung zu registrieren:

var builder = await TestApplication.CreateBuilderAsync(args);

// ...

builder.TestHost.AddDataConsumer(
    static serviceProvider => new CustomDataConsumer());

Die Factory nutzt den IServiceProvider, um Zugriff auf die Sammlung von Diensten zu erhalten, die die Testplattform bietet.

Von Bedeutung

Die Registrierungsreihenfolge ist wichtig, da die APIs in der Reihenfolge aufgerufen werden, in der sie registriert wurden.

Die IDataConsumer-Schnittstelle umfasst die folgenden Methoden:

public interface IDataConsumer : ITestHostExtension
{
    Type[] DataTypesConsumed { get; }

    Task ConsumeAsync(
        IDataProducer dataProducer,
        IData value,
        CancellationToken cancellationToken);
}

public interface IData
{
    string DisplayName { get; }
    string? Description { get; }
}

IDataConsumer ist ein Typ von ITestHostExtension, der als Basis für alle Testhosterweiterungen dient. Wie alle anderen Erweiterungspunkte erbt er ebenfalls von IExtension. Daher können Sie diese Erweiterung wie jede andere mithilfe der IExtension.IsEnabledAsync-API aktivieren oder deaktivieren.

DataTypesConsumed: Diese Eigenschaft gibt eine Liste der Typen (Type) zurück, die diese Erweiterung nutzen will. Sie entspricht IDataProducer.DataTypesProduced. Insbesondere kann ein IDataConsumer problemlos mehrere Typen aus verschiedenen IDataProducer-Instanzen abonnieren.

ConsumeAsync: Diese Methode wird ausgelöst, wenn Daten eines Typs, den der aktuelle Consumer abonniert hat, an den IMessageBus gepusht werden. Sie empfängt den IDataProducer, um Details zum Erzeuger der Datennutzlast sowie die IData-Nutzdaten selbst bereitzustellen. Wie Sie sehen, ist IData eine generische Platzhalterschnittstelle, die allgemeine informative Daten enthält. Die Möglichkeit, verschiedene IData-Typen zu pushen, bedeutet, dass der Consumer den Typ selbst aktivieren muss, um ihn in den richtigen Typ umzuwandeln und auf die spezifischen Informationen zuzugreifen.

Eine Beispielimplementierung eines Consumers, der die von einem TestNodeUpdateMessage erzeugte auswerten will, könnte wie folgt aussehen:

internal class CustomDataConsumer : IDataConsumer, IOutputDeviceDataProducer
{
    public Type[] DataTypesConsumed => new[] { typeof(TestNodeUpdateMessage) };
    ...
    public Task ConsumeAsync(
        IDataProducer dataProducer,
        IData value,
        CancellationToken cancellationToken)
    {
        var testNodeUpdateMessage = (TestNodeUpdateMessage)value;

        switch (testNodeUpdateMessage.TestNode.Properties.Single<TestNodeStateProperty>())
        {
            case InProgressTestNodeStateProperty _:
                {
                    ...
                    break;
                }
            case PassedTestNodeStateProperty _:
                {
                    ...
                    break;
                }
            case FailedTestNodeStateProperty failedTestNodeStateProperty:
                {
                    ...
                    break;
                }
            case SkippedTestNodeStateProperty _:
                {
                    ...
                    break;
                }
            ...
        }

        return Task.CompletedTask;
    }
...
}

Zu guter Letzt akzeptiert die API ein CancellationToken, das von der Erweiterung berücksichtigt werden muss.

Von Bedeutung

Es ist entscheidend, die Nutzdaten direkt innerhalb der ConsumeAsync-Methode zu verarbeiten. Der IMessageBus kann sowohl die synchrone als auch die asynchrone Verarbeitung verwalten und die Ausführung mit dem Testframework koordinieren. Zum Zeitpunkt der Erstellung dieses Dokuments ist der Nutzungs- bzw. Verbrauchsprozess vollständig asynchron und blockiert IMessageBus.Push nicht. Dies ist jedoch ein Implementierungsdetail, das sich aufgrund künftiger Anforderungen in Zukunft ändern kann. Die Plattform stellt jedoch sicher, dass diese Methode immer nur einmal aufgerufen wird, sodass eine komplexe Synchronisierung nicht erforderlich und die Skalierbarkeit der Consumer gewährleistet ist.

Warnung

Wenn Sie IDataConsumer in Verbindung mit ITestHostProcessLifetimeHandler innerhalb eines zusammengesetzten Erweiterungspunkts verwenden, ist es wichtig, alle Daten zu ignorieren, die nach der Ausführung von ITestSessionLifetimeHandler.OnTestSessionFinishingAsync empfangen werden. Die OnTestSessionFinishingAsync-Methode ist die letzte Möglichkeit, gesammelte Daten zu verarbeiten und neue Informationen an den IMessageBus zu übermitteln. Daher können alle Daten, die nach diesem Punkt genutzt werden, nicht von der Erweiterung verwendet werden.

Wenn Ihre Erweiterung eine intensive Initialisierung erfordert und Sie das async/await-Muster verwenden müssen, finden Sie unter Async extension initialization and cleanup weitere Informationen. Informationen zum Freigeben des Status zwischen Erweiterungspunkten finden Sie im Abschnitt CompositeExtensionFactory<T>.

ITestHostEnvironmentVariableProvider-Erweiterungen

ITestHostEnvironmentVariableProvider ist eine prozessexterne Erweiterung, mit der Sie benutzerdefinierte Umgebungsvariablen für den Testhost einrichten können. Die Verwendung dieses Erweiterungspunkts stellt sicher, dass die Testplattform einen neuen Host mit den entsprechenden Umgebungsvariablen initiiert (siehe hierzu den Abschnitt zur Architektur).

Verwenden Sie die folgende API, um eine benutzerdefinierte ITestHostEnvironmentVariableProvider-Schnittstelle zu registrieren:

var builder = await TestApplication.CreateBuilderAsync(args);

// ...

builder.TestHostControllers.AddEnvironmentVariableProvider(
    static serviceProvider => new CustomEnvironmentVariableForTestHost());

Die Factory nutzt den IServiceProvider, um Zugriff auf die Sammlung von Diensten zu erhalten, die die Testplattform bietet.

Von Bedeutung

Die Registrierungsreihenfolge ist wichtig, da die APIs in der Reihenfolge aufgerufen werden, in der sie registriert wurden.

Die ITestHostEnvironmentVariableProvider-Schnittstelle umfasst die folgenden Methoden und Typen:

public interface ITestHostEnvironmentVariableProvider : ITestHostControllersExtension, IExtension
{
    Task UpdateAsync(IEnvironmentVariables environmentVariables);

    Task<ValidationResult> ValidateTestHostEnvironmentVariablesAsync(
        IReadOnlyEnvironmentVariables environmentVariables);
}

public interface IEnvironmentVariables : IReadOnlyEnvironmentVariables
{
    void SetVariable(EnvironmentVariable environmentVariable);
    void RemoveVariable(string variable);
}

public interface IReadOnlyEnvironmentVariables
{
    bool TryGetVariable(
        string variable,
        [NotNullWhen(true)] out OwnedEnvironmentVariable? environmentVariable);
}

public sealed class OwnedEnvironmentVariable : EnvironmentVariable
{
    public IExtension Owner { get; }

    public OwnedEnvironmentVariable(
        IExtension owner,
        string variable,
        string? value,
        bool isSecret,
        bool isLocked);
}

public class EnvironmentVariable
{
    public string Variable { get; }
    public string? Value { get; }
    public bool IsSecret { get; }
    public bool IsLocked { get; }
}

ITestHostEnvironmentVariableProvider ist ein Typ von ITestHostControllersExtension, der als Basis für alle Testhostcontroller-Erweiterungen dient. Wie alle anderen Erweiterungspunkte erbt er ebenfalls von IExtension. Daher können Sie diese Erweiterung wie jede andere mithilfe der IExtension.IsEnabledAsync-API aktivieren oder deaktivieren.

Berücksichtigen Sie für diese API die folgenden Details:

UpdateAsync: Diese Aktualisierungs-API stellt eine Instanz des IEnvironmentVariables-Objekts bereit, über die Sie die Methoden SetVariable oder RemoveVariable aufrufen können. Wenn Sie SetVariable verwenden, müssen Sie ein Objekt vom Typ EnvironmentVariable übergeben, das die folgenden Angaben erfordert:

  • Variable: Der Name der Umgebungsvariable.
  • Value: Der Wert der Umgebungsvariable.
  • IsSecret: Gibt an, ob die Umgebungsvariable vertrauliche Informationen enthält, die nicht protokolliert werden oder nicht über TryGetVariable zugänglich sein sollen.
  • IsLocked: Legt fest, ob andere ITestHostEnvironmentVariableProvider-Erweiterungen diesen Wert ändern können.

ValidateTestHostEnvironmentVariablesAsync: Diese Methode wird aufgerufen, nachdem alle UpdateAsync-Methoden der registrierten ITestHostEnvironmentVariableProvider-Instanzen aufgerufen wurden. Sie ermöglicht es Ihnen, die korrekte Einrichtung der Umgebungsvariablen zu überprüfen. Sie akzeptiert ein Objekt, das die IReadOnlyEnvironmentVariables-Schnittstelle implementiert, die die TryGetVariable-Methode zum Abrufen bestimmter Umgebungsvariableninformationen mit dem OwnedEnvironmentVariable-Objekttyp bereitstellt. Nach der Überprüfung geben Sie ein ValidationResult zurück, das alle Fehlerursachen enthält.

Hinweis

Die Testplattform implementiert und registriert standardmäßig den SystemEnvironmentVariableProvider. Dieser Anbieter lädt alle aktuellen Umgebungsvariablen. Als erster registrierter Anbieter wird er zuerst ausgeführt und gewährt allen anderen ITestHostEnvironmentVariableProvider-Benutzererweiterungen den Zugriff auf die Standardumgebungsvariablen.

Wenn Ihre Erweiterung eine intensive Initialisierung erfordert und Sie das async/await-Muster verwenden müssen, finden Sie unter Async extension initialization and cleanup weitere Informationen. Informationen zum Freigeben des Status zwischen Erweiterungspunkten finden Sie im Abschnitt CompositeExtensionFactory<T>.

ITestHostProcessLifetimeHandler-Erweiterungen

ITestHostProcessLifetimeHandler ist eine prozessexterne Erweiterung, mit der Sie den Testhostprozess aus externer Sicht beobachten können. Dadurch wird sichergestellt, dass sich durch den getesteten Code verursachte mögliche Abstürze oder ein Hängenbleiben nicht auf Ihre Erweiterung auswirken. Wenn Sie diesen Erweiterungspunkt verwenden, wird die Testplattform aufgefordert, einen neuen Host zu initiieren (siehe hierzu den Abschnitt zur Architektur).

Verwenden Sie die folgende API, um eine benutzerdefinierte ITestHostProcessLifetimeHandler-Schnittstelle zu registrieren:

var builder = await TestApplication.CreateBuilderAsync(args);

// ...

builder.TestHostControllers.AddProcessLifetimeHandler(
    static serviceProvider => new CustomMonitorTestHost());

Die Factory nutzt den IServiceProvider, um Zugriff auf die Sammlung von Diensten zu erhalten, die die Testplattform bietet.

Von Bedeutung

Die Registrierungsreihenfolge ist wichtig, da die APIs in der Reihenfolge aufgerufen werden, in der sie registriert wurden.

Die ITestHostProcessLifetimeHandler-Schnittstelle umfasst die folgenden Methoden:

public interface ITestHostProcessLifetimeHandler : ITestHostControllersExtension
{
    Task BeforeTestHostProcessStartAsync(CancellationToken cancellationToken);

    Task OnTestHostProcessStartedAsync(
        ITestHostProcessInformation testHostProcessInformation,
        CancellationToken cancellation);

    Task OnTestHostProcessExitedAsync(
        ITestHostProcessInformation testHostProcessInformation,
        CancellationToken cancellation);
}

public interface ITestHostProcessInformation
{
    int PID { get; }
    int ExitCode { get; }
    bool HasExitedGracefully { get; }
}

ITestHostProcessLifetimeHandler ist ein Typ von ITestHostControllersExtension, der als Basis für alle Testhostcontroller-Erweiterungen dient. Wie alle anderen Erweiterungspunkte erbt er ebenfalls von IExtension. Daher können Sie diese Erweiterung wie jede andere mithilfe der IExtension.IsEnabledAsync-API aktivieren oder deaktivieren.

Beachten Sie für diese API die folgenden Details:

BeforeTestHostProcessStartAsync: Diese Methode wird aufgerufen, bevor die Testplattform die Testhosts initiiert.

OnTestHostProcessStartedAsync: Diese Methode wird unmittelbar nach dem Start des Testhosts aufgerufen. Diese Methode bietet ein Objekt, das die ITestHostProcessInformation-Schnittstelle implementiert, die wichtige Details zum Testhostprozessergebnis bereitstellt.

Von Bedeutung

Durch den Aufruf dieser Methode wird die Ausführung des Testhosts nicht angehalten. Wenn Sie die Ausführung anhalten müssen, sollten Sie eine prozessinterne Erweiterung wie ITestApplicationLifecycleCallbacks registrieren und sie mit der prozessexternen Erweiterung synchronisieren.

OnTestHostProcessExitedAsync: Diese Methode wird aufgerufen, wenn die Ausführung der Testsammlung abgeschlossen ist. Sie stellt ein Objekt bereit, das der ITestHostProcessInformation-Schnittstelle entspricht, die wichtige Details zum Ergebnis des Testhostprozesses übermittelt.

Die ITestHostProcessInformation-Schnittstelle stellt die folgenden Details bereit:

  • PID: Die Prozess-ID des Testhosts.
  • ExitCode: Der Exitcode des Prozesses. Dieser Wert ist nur in der OnTestHostProcessExitedAsync-Methode verfügbar. Der Versuch, innerhalb der OnTestHostProcessStartedAsync-Methode darauf zuzugreifen, führt zu einer Ausnahme.
  • HasExitedGracefully: Ein boolescher Wert, der angibt, ob der Testhost abgestürzt ist. Wenn der Wert „true” ist, wurde der Testhost nicht ordnungsgemäß beendet.

Ausführungsreihenfolge der Erweiterungen

Die Testplattform besteht aus einem Testframework und einer beliebigen Anzahl von Erweiterungen, die prozessintern oder prozessextern ausgeführt werden können. In diesem Dokument wird die Reihenfolge der Aufrufe aller möglichen Erweiterungspunkte beschrieben, um zu verdeutlichen, wann der Aufruf eines Features erwartet wird:

  1. ITestHostEnvironmentVariableProvider.UpdateAsync: Prozessextern
  2. ITestHostEnvironmentVariableProvider.ValidateTestHostEnvironmentVariablesAsync: Prozessextern
  3. ITestHostProcessLifetimeHandler.BeforeTestHostProcessStartAsync: Prozessextern
  4. Start des Testhostprozesses
  5. ITestHostProcessLifetimeHandler.OnTestHostProcessStartedAsync: Prozessextern; dieses Ereignis kann die Aktionen von prozessinternen Erweiterungen abhängig von Racebedingungen miteinander verknüpfen.
  6. ITestApplicationLifecycleCallbacks.BeforeRunAsync: Prozessintern
  7. ITestSessionLifetimeHandler.OnTestSessionStartingAsync: Prozessintern
  8. ITestFramework.CreateTestSessionAsync: Prozessintern
  9. ITestFramework.ExecuteRequestAsync: Prozessintern; diese Methode kann einmal oder mehrmals aufgerufen werden. An diesem Punkt übermittelt das Testframework Informationen an den IMessageBus, die vom IDataConsumer genutzt werden können.
  10. ITestFramework.CloseTestSessionAsync: Prozessintern
  11. ITestSessionLifetimeHandler.OnTestSessionFinishingAsync: Prozessintern
  12. ITestApplicationLifecycleCallbacks.AfterRunAsync: Prozessintern
  13. Die prozessinterne Bereinigung umfasst das Aufrufen von „dispose” und IAsyncCleanableExtension für alle Erweiterungspunkte.
  14. ITestHostProcessLifetimeHandler.OnTestHostProcessExitedAsync: Prozessextern
  15. Die prozessexterne Bereinigung umfasst das Aufrufen von „dispose” und IAsyncCleanableExtension für alle Erweiterungspunkte.

Hilfsprogramme für Erweiterungen

Die Testplattform bietet eine Reihe von Hilfsklassen und -schnittstellen, um die Implementierung von Erweiterungen zu vereinfachen. Diese Hilfsprogramme beschleunigen den Entwicklungsprozess und stellen sicher, dass die Erweiterung den Standards der Plattform entspricht.

Asynchrone Initialisierung und Bereinigung von Erweiterungen

Die Erstellung des Testframeworks und der Erweiterungen über Factorys entspricht dem standardmäßigen .NET-Objekterstellungsmechanismus, bei dem synchrone Konstruktoren verwendet werden. Wenn eine Erweiterung eine intensive Initialisierung erfordert (z. B. mit Zugriff auf das Dateisystem oder Netzwerk), kann sie das async/await-Muster nicht im Konstruktor verwenden, da Konstruktoren „void” zurückgeben, nicht Task.

Daher stellt die Testplattform eine Methode zum Initialisieren einer Erweiterung mit dem async/await-Muster über eine einfache Schnittstelle bereit. Aus Gründen der Symmetrie bietet sie auch eine asynchrone Schnittstelle für die Bereinigung, die Erweiterungen nahtlos implementieren können.

public interface IAsyncInitializableExtension
{
    Task InitializeAsync();
}

public interface IAsyncCleanableExtension
{
    Task CleanupAsync();
}

IAsyncInitializableExtension.InitializeAsync: Diese Methode wird immer nach der Erstellungsfactory aufgerufen.

IAsyncCleanableExtension.CleanupAsync: Diese Methode wird immer mindestens einmal während der Beendigung der Testsitzung vor dem standardmäßigen DisposeAsync- oder Dispose-Aufruf aufgerufen.

Von Bedeutung

Wie die Dispose-Standardmethode kann CleanupAsync mehrmals aufgerufen werden. Wenn die CleanupAsync-Methode eines Objekts mehrmals aufgerufen wird, muss das Objekt alle Aufrufe nach dem ersten Aufruf ignorieren. Das Objekt darf keine Ausnahme auslösen, wenn seine CleanupAsync-Methode mehrmals aufgerufen wird.

Hinweis

Standardmäßig ruft die Testplattform die DisposeAsync-Methode auf, sofern diese verfügbar ist, oder die Dispose-Methode, wenn diese implementiert ist. Beachten Sie, dass die Testplattform nicht beide dispose-Methoden aufruft, sondern die asynchrone Methode priorisiert, sofern diese implementiert ist.

CompositeExtensionFactory<T>

Wie im Abschnitt zu den Erweiterungen beschrieben, ermöglicht Ihnen die Testplattform das Implementieren von Schnittstellen, um sowohl prozessinterne als auch prozessexterne benutzerdefinierte Erweiterungen zu integrieren.

Jede Schnittstelle stellt ein bestimmtes Feature bereit, und gemäß dem .NET-Entwurf implementieren Sie diese Schnittstelle in einem spezifischen Objekt. Wie in den entsprechenden Abschnitten beschrieben, können Sie die Erweiterung selbst mithilfe der spezifischen Registrierungs-API AddXXX über das TestHost- oder TestHostController-Objekt von ITestApplicationBuilder registrieren.

Wenn Sie zwischen zwei Erweiterungen den Status freigeben müssen, wird die Freigabe jedoch durch die Tatsache erschwert, dass Sie unterschiedliche Objekte implementieren und registrieren können, die verschiedene Schnittstellen implementieren. Ohne jegliche Unterstützung würden Sie eine Möglichkeit benötigen, eine Erweiterung an die andere weiterzugeben, um Informationen auszutauschen, was den Entwurf verkompliziert.

Die Testplattform bietet daher eine hochentwickelte Methode zum Implementieren mehrerer Erweiterungspunkte mit demselben Typ, die die Datenfreigabe vereinfacht. Dazu müssen Sie lediglich die CompositeExtensionFactory<T> verwenden, die dann mit der gleichen API wie eine einzelne Schnittstellenimplementierung registriert werden kann.

Angenommen, Sie haben einen Typ, der sowohl ITestSessionLifetimeHandler als auch IDataConsumer implementiert. Dies ist ein gängiges Szenario, da Sie häufig Informationen vom Testframework sammeln und Ihr Artefakt dann beim Abschluss der Testsitzung mithilfe des IMessageBus innerhalb von ITestSessionLifetimeHandler.OnTestSessionFinishingAsync senden möchten.

Dazu implementieren Sie normalerweise die Schnittstellen:

internal class CustomExtension : ITestSessionLifetimeHandler, IDataConsumer, ...
{
   ...
}

Nachdem Sie die CompositeExtensionFactory<CustomExtension> für Ihren Typ erstellt haben, können Sie sie mit den APIs IDataConsumer und ITestSessionLifetimeHandler registrieren, die eine Überladung für die CompositeExtensionFactory<T> bieten:

var builder = await TestApplication.CreateBuilderAsync(args);

// ...

var factory = new CompositeExtensionFactory<CustomExtension>(serviceProvider => new CustomExtension());

builder.TestHost.AddTestSessionLifetimeHandle(factory);
builder.TestHost.AddDataConsumer(factory);

Der Factorykonstruktor verwendet den IServiceProvider, um auf die von der Testplattform bereitgestellten Dienste zuzugreifen.

Die Testplattform ist für die Verwaltung des Lebenszyklus der zusammengesetzten Erweiterung verantwortlich.

Da die Testplattform sowohl prozessinterne als auch prozessexterne Erweiterungen unterstützt, ist es nicht möglich, Erweiterungspunkte beliebig zu kombinieren. Die Erstellung und Verwendung von Erweiterungen ist vom Hosttyp abhängig, d. h. Sie können nur prozessinterne (TestHost) und prozessexterne (TestHostController) Erweiterungen gruppieren.

Folgende Kombinationen sind möglich:

  • Für ITestApplicationBuilder.TestHost können Sie IDataConsumer und ITestSessionLifetimeHandler kombinieren.
  • Für ITestApplicationBuilder.TestHostControllers können Sie ITestHostEnvironmentVariableProvider und ITestHostProcessLifetimeHandler kombinieren.