Condividi tramite


Ciclo di vita di MSTest

MSTest offre un ciclo di vita ben definito per le classi di test e i metodi di test, consentendo di eseguire operazioni di installazione e disinstallazione in varie fasi dell'esecuzione dei test. Comprendere il ciclo di vita consente di scrivere test efficienti ed evitare problemi comuni.

Panoramica del ciclo di vita

Il ciclo di vita raggruppa in quattro fasi, eseguito dal livello più alto (assembly) al livello più basso (metodo di test):

  1. Livello di assembly: viene eseguito una sola volta quando l'assembly di test carica e scarica
  2. Livello di classe: viene eseguito una sola volta per ogni classe di test
  3. Livello di test globale: viene eseguito prima e dopo ogni metodo di test nell'assembly
  4. Livello di test: viene eseguito per ogni metodo di test (inclusa ogni riga di dati nei test con parametri)

Ciclo di vita a livello di assembly

I metodi del ciclo di vita degli assembly vengono eseguiti una sola volta quando l'assembly di test viene caricato e scaricato. Usarli per un'installazione occasionale costosa, ad esempio l'inizializzazione del database o l'avvio del servizio.

AssemblyInitialize e 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!);
    }
}

Requisiti

  • I metodi devono essere public static
  • Tipo restituito: void, Tasko ValueTask (MSTest v3.3+)
  • AssemblyInitialize richiede un TestContext parametro
  • AssemblyCleanup accetta parametri zero o un TestContext parametro (MSTest 3.8+)
  • Solo uno di ogni attributo consentito per ogni assembly
  • Deve trovarsi in una classe contrassegnata con [TestClass]

Suggerimento

Analizzatori correlati:

  • MSTEST0012 : convalida la AssemblyInitialize firma.
  • MSTEST0013 : convalida la AssemblyCleanup firma.

Ciclo di vita a livello di classe

I metodi del ciclo di vita della classe vengono eseguiti una volta per ogni classe di test, prima e dopo tutti i metodi di test in tale classe. Utilizzare questi per la configurazione comune ai test all'interno di una classe.

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

Requisiti

  • I metodi devono essere public static
  • Tipo restituito: void, Tasko ValueTask (MSTest v3.3+)
  • ClassInitialize richiede un TestContext parametro
  • ClassCleanup accetta parametri zero o un TestContext parametro (MSTest 3.8+)
  • Solo uno di ogni attributo consentito per ogni classe

Comportamento di ereditarietà

Controllare se ClassInitialize viene eseguito per le classi derivate usando 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
    }
}
InheritanceBehavior Description
None (impostazione predefinita) L'inizializzazione viene eseguita solo per la classe dichiarativa
BeforeEachDerivedClass Inizializzare le esecuzioni prima di ogni classe derivata

Suggerimento

Analizzatori correlati:

  • MSTEST0010 : convalida la ClassInitialize firma.
  • MSTEST0011 : convalida la ClassCleanup firma.
  • MSTEST0034 : consiglia di usare ClassCleanupBehavior.EndOfClass.

Ciclo di vita globale a livello di test

Annotazioni

Gli attributi del ciclo di vita dei test globali sono stati introdotti in MSTest 3.10.0.

I metodi del ciclo di vita dei test globali vengono eseguiti prima e dopo ogni metodo di test nell'intero assembly, senza dover aggiungere codice a ogni classe di test.

GlobalTestInitialize e 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}");
    }
}

Requisiti

  • I metodi devono essere public static
  • Tipo restituito: void, Tasko ValueTask
  • Deve avere esattamente un TestContext parametro
  • Deve trovarsi in una classe contrassegnata con [TestClass]
  • Nell'assembly sono consentiti più metodi con questi attributi

Annotazioni

Se esistono più metodi GlobalTestInitialize o GlobalTestCleanup, l'ordine di esecuzione non è garantito. Non TimeoutAttribute è supportato nei GlobalTestInitialize metodi.

Suggerimento

Analizzatore correlato: MSTEST0050 : convalida i metodi di fixture di test globali.

Ciclo di vita a livello di test

Il ciclo di vita a livello di test viene eseguito per ogni metodo di test. Per i test con parametri, il ciclo di vita viene eseguito per ogni riga di dati.

Fase di installazione

Usare TestInitialize o un costruttore per la configurazione per 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);
    }
}

Costruttore e TestInitialize:

Aspetto Costruttore TestInitialize
Supporto asincrono NO Yes
Supporto del timeout NO Sì (con [Timeout] attributo)
Ordine di esecuzione First Dopo il costruttore
Ereditarietà Base quindi derivata Base quindi derivata
Comportamento delle eccezioni La funzione Cleanup e la funzione Dispose non vengono effettuate (non esiste alcuna istanza) Pulizia e smaltimento sono ancora in corso

Suggerimento

Quale approccio è consigliabile usare? I costruttori sono in genere preferiti perché consentono di usare readonly campi, che impongono l'immutabilità e rendono più semplice il ragionamento sulla tua classe di test. Usare TestInitialize quando è necessario un supporto asincrono di inizializzazione o timeout.

È anche possibile combinare entrambi gli approcci: usare il costruttore per l'inizializzazione sincrona semplice dei readonly campi e TestInitialize per un'installazione asincrona aggiuntiva che dipende da tali campi.

Facoltativamente, è possibile abilitare gli analizzatori del codice per applicare un approccio coerente:

  • MSTEST0019 - Preferire i metodi di TestInitialize ai costruttori
  • MSTEST0020 : preferisce i costruttori rispetto ai metodi TestInitialize

Fase di esecuzione

Il metodo di test viene eseguito al termine dell'installazione. Per i metodi di test async, MSTest attende il valore restituito Task o ValueTask.

Avvertimento

Per impostazione predefinita, i metodi di test asincroni non dispongono di un oggetto SynchronizationContext . Questo non si applica ai UITestMethod test in UWP e WinUI, che vengono eseguiti nel thread dell'interfaccia utente.

Fase di pulizia

Utilizzare TestCleanup o IDisposable/IAsyncDisposable per la pulizia dei 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);
    }
}

Ordine di esecuzione della pulizia (derivato dalla base):

  1. TestCleanup (classe derivata)
  2. TestCleanup (classe base)
  3. DisposeAsync (se implementato)
  4. Dispose (se implementato)

Suggerimento

Facoltativamente, è possibile abilitare gli analizzatori del codice per applicare un approccio di pulizia coerente:

  • MSTEST0021 - Preferire il metodo Dispose rispetto ai metodi TestCleanup
  • MSTEST0022 - Preferi TestCleanup rispetto ai metodi Dispose

Se sono abilitati analizzatori non MSTest, ad esempio le regole di analisi del codice .NET, è possibile che venga visualizzato l'avviso CA1001 che suggerisce di implementare il modello dispose quando la classe di test è proprietaria di risorse eliminabili. Questo è il comportamento previsto ed è necessario seguire le indicazioni dell'analizzatore.

Completare l'ordine a livello di test

  1. Creare un'istanza della classe di test (costruttore)
  2. Impostare la proprietà TestContext (se presente)
  3. Esegui metodi GlobalTestInitialize
  4. Eseguire i metodi TestInitialize dalla base al derivato
  5. Eseguire il metodo di test
  6. Aggiornare TestContext con i risultati (ad esempio, proprietà Outcome)
  7. Esegui metodi TestCleanup (dal derivato alla base)
  8. Esegui metodi GlobalTestCleanup
  9. Esecuzione DisposeAsync (se implementata)
  10. Esecuzione Dispose (se implementata)

Suggerimento

Analizzatori correlati:

  • MSTEST0008 : convalida la TestInitialize firma.
  • MSTEST0009 : convalida la TestCleanup firma.
  • MSTEST0063 : convalida il costruttore della classe di test.

Procedure consigliate

  1. Usare l'ambito appropriato: Colloca la configurazione al livello più alto che abbia senso per evitare lavori ridondanti.

  2. Mantenere la configurazione veloce: l'installazione a esecuzione prolungata influisce su tutti i test. Prendere in considerazione l'inizializzazione differita per le risorse costose.

  3. Pulire correttamente: pulire sempre le risorse per evitare interferenze di test e perdite di memoria.

  4. Gestione asincrona corretta: Usare async Task tipi di ritorno, non async void, per metodi asincroni del ciclo di vita.

  5. Prendere in considerazione l'isolamento dei test: ogni test deve essere indipendente. Evitare lo stato modificabile condiviso tra i test.

  6. Usare GlobalTest con moderazione: i metodi del ciclo di vita globale vengono eseguiti per ogni test, pertanto è consigliabile mantenerli leggeri.

Vedere anche