Teilen über


MSTest-Lebenszyklus

MSTest bietet einen gut definierten Lebenszyklus für Testklassen und Testmethoden, sodass Sie Setup- und Teardownvorgänge in verschiedenen Phasen der Testausführung durchführen können. Wenn Sie den Lebenszyklus verstehen, können Sie effiziente Tests schreiben und häufige Fallstricke vermeiden.

Übersicht über den Lebenszyklus

Der Lebenszyklus gliedert sich in vier Phasen, die von der höchsten Ebene (Assembly) bis zur niedrigsten Ebene (Testmethode) ausgeführt werden.

  1. Assemblyebene: Wird einmal ausgeführt, wenn die Testassembly geladen und entladen wird
  2. Klassenebene: Wird einmal pro Testklasse ausgeführt
  3. Globale Testebene: Wird vor und nach jeder Testmethode in der Assembly ausgeführt
  4. Testebene: Wird für jede Testmethode ausgeführt (einschließlich jeder Datenzeile in parametrisierten Tests)

Lebenszyklus auf Assemblyebene

Assembly-Lebenszyklusmethoden werden einmal ausgeführt, wenn die Testassembly geladen und entladen wird. Verwenden Sie diese für teure einmalige Einrichtung, z. B. datenbankinitialisierung oder Dienststart.

AssemblyInitialize und AssemblyCleanup

[TestClass]
public class AssemblyLifecycleExample
{
    private static IHost? _host;

    [AssemblyInitialize]
    public static async Task AssemblyInit(TestContext context)
    {
        // Runs once before any tests in the assembly
        _host = await StartTestServerAsync();
        context.WriteLine("Test server started");
    }

    [AssemblyCleanup]
    public static async Task AssemblyCleanup(TestContext context)
    {
        // Runs once after all tests complete
        // TestContext parameter available in MSTest 3.8+
        if (_host != null)
        {
            await _host.StopAsync();
        }
    }

    private static Task<IHost> StartTestServerAsync()
    {
        // Server initialization
        return Task.FromResult<IHost>(null!);
    }
}

Anforderungen

  • Methoden müssen public static
  • Rückgabetyp: void, , Taskoder ValueTask (MSTest v3.3+)
  • AssemblyInitialize erfordert einen TestContext Parameter
  • AssemblyCleanup akzeptiert Nullparameter oder einen TestContext Parameter (MSTest 3.8+)
  • Pro Baugruppe ist nur ein Attribut zulässig.
  • Muss in einer Klasse vorhanden sein, die mit [TestClass] markiert ist.

Tipp

Verwandte Analysegeräte:

  • MSTEST0012 – überprüft AssemblyInitialize die Signatur.
  • MSTEST0013 – überprüft AssemblyCleanup die Signatur.

Lebenszyklus auf Klassenebene

Klassenlebenszyklusmethoden werden einmal pro Testklasse ausgeführt, bevor und nach allen Testmethoden in dieser Klasse. Verwenden Sie diese für das Setup, das für Tests in einer Klasse geteilt wird.

ClassInitialize und ClassCleanup

[TestClass]
public class ClassLifecycleExample
{
    private static HttpClient? _client;

    [ClassInitialize]
    public static void ClassInit(TestContext context)
    {
        // Runs once before any tests in this class
        _client = new HttpClient
        {
            BaseAddress = new Uri("https://api.example.com")
        };
    }

    [ClassCleanup]
    public static void ClassCleanup()
    {
        // Runs after all tests in this class complete
        _client?.Dispose();
    }

    [TestMethod]
    public async Task GetUsers_ReturnsSuccess()
    {
        var response = await _client!.GetAsync("/users");
        Assert.IsTrue(response.IsSuccessStatusCode);
    }
}

Anforderungen

  • Methoden müssen public static
  • Rückgabetyp: void, , Taskoder ValueTask (MSTest v3.3+)
  • ClassInitialize erfordert einen TestContext Parameter
  • ClassCleanup akzeptiert Nullparameter oder einen TestContext Parameter (MSTest 3.8+)
  • Pro Klasse ist nur ein Attribut zulässig.

Vererbungsverhalten

Steuern, ob ClassInitialize für abgeleitete Klassen mithilfe von InheritanceBehavior:

[TestClass]
public class BaseTestClass
{
    [ClassInitialize(InheritanceBehavior.BeforeEachDerivedClass)]
    public static void BaseClassInit(TestContext context)
    {
        // Runs before each derived class's tests
    }
}

[TestClass]
public class DerivedTestClass : BaseTestClass
{
    [TestMethod]
    public void DerivedTest()
    {
        // BaseClassInit runs before this class's tests
    }
}
Vererbungsverhalten Description
None (Standardwert) Initialisieren wird nur für die deklarierende Klasse ausgeführt
BeforeEachDerivedClass Initialisieren von Läufen vor jeder abgeleiteten Klasse

Tipp

Verwandte Analysegeräte:

  • MSTEST0010 – überprüft ClassInitialize die Signatur.
  • MSTEST0011 – überprüft ClassCleanup die Signatur.
  • MSTEST0034 - empfiehlt die Verwendung ClassCleanupBehavior.EndOfClass.

Globaler Lebenszyklus auf Testebene

Hinweis

Globale Testlebenszyklusattribute wurden in MSTest 3.10.0 eingeführt.

Globale Testlebenszyklusmethoden werden vor und nach jeder Testmethode in der gesamten Assembly ausgeführt, ohne dass jeder Testklasse Code hinzugefügt werden muss.

GlobalTestInitialize und GlobalTestCleanup

[TestClass]
public class GlobalTestLifecycleExample
{
    [GlobalTestInitialize]
    public static void GlobalTestInit(TestContext context)
    {
        // Runs before every test method in the assembly
        context.WriteLine($"Starting test: {context.TestName}");
    }

    [GlobalTestCleanup]
    public static void GlobalTestCleanup(TestContext context)
    {
        // Runs after every test method in the assembly
        context.WriteLine($"Finished test: {context.TestName}");
    }
}

Anforderungen

  • Methoden müssen public static
  • Rückgabetyp: void, , Taskoder ValueTask
  • Muss genau einen TestContext Parameter aufweisen
  • Muss in einer mit [TestClass] gekennzeichneten Klasse sein
  • Für das Assembly sind mehrere Methoden mit diesen Attributen zulässig.

Hinweis

Wenn mehrere Methoden GlobalTestInitialize oder GlobalTestCleanup vorhanden sind, wird die Ausführungsreihenfolge nicht garantiert. Dies TimeoutAttribute wird für GlobalTestInitialize Methoden nicht unterstützt.

Tipp

Verwandter Analysator: MSTEST0050 - überprüft globale Prüfgerätemethoden.

Lebenszyklus der Teststufe

Der Lebenszyklus wird auf Testebene für jede Testmethode ausgeführt. Bei parametrisierten Tests wird der Lebenszyklus für jede Datenzeile ausgeführt.

Einrichtungsphase

Verwenden Sie TestInitialize oder einen Konstruktor zur Einrichtung für jeden Test:

[TestClass]
public class TestLevelSetupExample
{
    private Calculator? _calculator;

    public TestLevelSetupExample()
    {
        // Constructor runs before TestInitialize
        // Use for simple synchronous initialization
    }

    [TestInitialize]
    public async Task TestInit()
    {
        // Runs before each test method
        // Supports async, attributes like Timeout
        _calculator = new Calculator();
        await _calculator.InitializeAsync();
    }

    [TestMethod]
    public void Add_TwoNumbers_ReturnsSum()
    {
        var result = _calculator!.Add(2, 3);
        Assert.AreEqual(5, result);
    }
}

Konstruktor vs. TestInitialize:

Aspekt Konstruktor TestInitialize
Asynchrone Unterstützung Nein Yes
Timeoutunterstützung Nein Ja (mit [Timeout] Attribut)
Ausführungsreihenfolge First (Erster) Nach Konstruktor
Vererbung Basis dann abgeleitet Basis dann abgeleitet
Ausnahmeverhalten Bereinigung und Dispose werden nicht ausgeführt (es ist keine Instanz vorhanden) Bereinigen und Entsorgen werden weiterhin ausgeführt

Tipp

Welchen Ansatz sollte ich verwenden? Konstruktoren werden im Allgemeinen bevorzugt, da sie die Verwendung von readonly Feldern ermöglichen, was die Unveränderlichkeit erzwingt und es einfacher macht, die Testklasse nachzuvollziehen. Verwenden Sie diese Methode TestInitialize , wenn Sie asynchrone Initialisierungs- oder Timeoutunterstützung benötigen.

Sie können auch beide Ansätze kombinieren: Verwenden Sie den Konstruktor für die einfache synchrone Initialisierung von readonly Feldern und TestInitialize für zusätzliche asynchrone Einrichtung, die von diesen Feldern abhängt.

Sie können optional Codeanalysatoren aktivieren, um einen konsistenten Ansatz zu erzwingen:

  • MSTEST0019 - TestInitialize-Methoden gegenüber Konstruktoren bevorzugen
  • MSTEST0020 – Bevorzugen von Konstruktoren gegenüber TestInitialize-Methoden

Ausführungsphase

Die Testmethode wird nach Abschluss des Setups ausgeführt. Für async Testmethoden wartet MSTest auf die zurückgegebene Task oder ValueTask.

Warnung

Asynchrone Testmethoden verfügen nicht standardmäßig über eine SynchronizationContext . Dies gilt nicht für UITestMethod Tests in UWP und WinUI, die im UI-Thread ausgeführt werden.

Bereinigungsphase

Verwenden TestCleanup oder IDisposable/IAsyncDisposable für die Bereinigung pro Test:

[TestClass]
public class TestLevelCleanupExample
{
    private HttpClient? _client;

    [TestInitialize]
    public void TestInit()
    {
        _client = new HttpClient();
    }

    [TestCleanup]
    public void TestCleanup()
    {
        if (_client != null)
        {
            _client.Dispose();
        }
    }

    [TestMethod]
    public async Task GetData_ReturnsSuccess()
    {
        var response = await _client!.GetAsync("https://example.com");
        Assert.IsTrue(response.IsSuccessStatusCode);
    }
}

Bereinigungsausführungsreihenfolge (abgeleitet zur Basis):

  1. TestCleanup (abgeleitete Klasse)
  2. TestCleanup (Basisklasse)
  3. DisposeAsync (falls implementiert)
  4. Dispose (falls implementiert)

Tipp

Sie können Code-Analysewerkzeuge aktivieren, um eine konsistente Bereinigungsstrategie zu erzwingen.

  • MSTEST0021 – Die Verwendung von Dispose-Methoden gegenüber TestCleanup-Methoden bevorzugen
  • MSTEST0022 - TestCleanup über Dispose-Methoden bevorzugen

Wenn Sie nicht-MSTest-Analysetools aktiviert haben, z. B. .NET-Codeanalyseregeln, wird möglicherweise CA1001 vorgeschlagen, das Dispose-Muster zu implementieren, wenn Ihre Testklasse freigebbare Ressourcen besitzt. Dies ist ein erwartetes Verhalten, und Sie sollten die Anleitungen des Analyzers befolgen.

Vollständige Reihenfolge auf Testebene

  1. Erstellen einer Instanz der Testklasse (Konstruktor)
  2. Eigenschaft TestContext festlegen (falls vorhanden)
  3. Ausführen von GlobalTestInitialize Methoden
  4. Führe TestInitialize-Methoden (von Basis zu abgeleitet) aus
  5. Testmethode ausführen
  6. Aktualisieren Sie TestContext mit Ergebnissen (z. B. Outcome-Eigenschaft)
  7. Ausführen von TestCleanup Methoden (abgeleitet auf Basis)
  8. Ausführen von GlobalTestCleanup Methoden
  9. DisposeAsync ausführen (falls implementiert)
  10. Ausführen Dispose (falls implementiert)

Tipp

Verwandte Analysegeräte:

  • MSTEST0008 – überprüft TestInitialize die Signatur.
  • MSTEST0009 – überprüft TestCleanup die Signatur.
  • MSTEST0063 – überprüft den Testklassenkonstruktor.

Bewährte Methoden

  1. Verwenden Sie den geeigneten Bereich: Richten Sie die Konfiguration auf der sinnvoll höchsten Ebene ein, um redundante Arbeit zu vermeiden.

  2. Halten Sie die Einrichtung schnell: Eine langwierige Einrichtung wirkt sich auf alle Tests aus. Erwägen Sie die faule Initialisierung für teure Ressourcen.

  3. Sorgfältig bereinigen: Entfernen Sie immer Ressourcen, um Teststörungen und Speicherverluste zu verhindern.

  4. Behandeln Sie asynchrone Vorgänge ordnungsgemäß: Verwenden Sie async Task Rückgabetypen, nicht async void, für asynchrone Lebenszyklus-Methoden.

  5. Erwägen Sie die Testisolation: Jeder Test sollte unabhängig sein. Vermeiden Sie gemeinsamen änderbaren Zustand zwischen Tests.

  6. Verwenden Sie GlobalTest sparsam: Globale Lebenszyklusmethoden werden für jeden Test ausgeführt, daher sollten sie ressourcenschonend bleiben.

Siehe auch