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.

Attrezzature di assemblaggio condivise con AssemblyFixtureProvider

Annotazioni

È AssemblyFixtureProviderAttribute stato introdotto in MSTest 4.3.0.

Per impostazione predefinita, [AssemblyInitialize] e [AssemblyCleanup] i metodi devono essere dichiarati nell'assembly di test che li usa. AssemblyFixtureProviderAttribute Consente di dichiarare questi metodi una sola volta in una libreria condivisa e di individuarli ed eseguirli una sola volta per ogni assembly di test che usa. Applica l'attributo a livello di assembly e indirizzalo al tipo che ospita i metodi [AssemblyInitialize]/[AssemblyCleanup].

// In a shared library.
public static class SharedAssemblyFixtures
{
    [AssemblyInitialize]
    public static void Init(TestContext context)
    {
        // Runs once per consuming test assembly.
    }

    [AssemblyCleanup]
    public static void Cleanup()
    {
    }
}
// In each test assembly that should run the shared fixtures.
[assembly: AssemblyFixtureProvider(typeof(SharedAssemblyFixtures))]

L'attributo consente più provider per ogni assembly, in modo da poter creare fixture a partire da più tipi condivisi.

Avvertimento

A partire da MSTest 4.4, MSTest ignora il rilevamento AssemblyFixtureProviderAttribute quando il codice dinamico non è supportato, incluse le esecuzioni Native AOT. Usare i metodi del fixture dichiarati direttamente nell'assembly di test per Native AOT. L'analizzatore MSTEST0072 segnala le configurazioni che può rilevare in fase di compilazione.

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()
    {
        HttpResponseMessage 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

Annotazioni

A partire da MSTest 4.2, MSTest segnala errori in ClassCleanup e AssemblyCleanup come risultato di test separato. Il risultato separato semplifica l'identificazione degli errori di pulizia nei report di Esplora test e TRX.

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. A partire da MSTest 4.4, i metodi globali di fixture supportano [Timeout] e le specifiche impostazioni di timeout della fixture globale.

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.

Creare classi di test dai servizi host

A partire da MSTest 4.5, il pacchetto sperimentale MSTest.Extensions.Hosting consente a un contenitore di proprietà dell'applicazione Microsoft.Extensions.Hosting di creare classi di test MSTest.

Registrare l'integrazione nello stesso host che esegue MTP:

HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Services.AddSingleton<MyApplicationService>();
builder.Services.AddMSTestTestClassInjection();

Una classe di test può richiedere servizi registrati e un parametro esatto TestContext facoltativo tramite un costruttore pubblico:

[TestClass]
public sealed class MyTests(MyApplicationService service, TestContext testContext)
{
    [TestMethod]
    public void ServiceIsAvailable() => Assert.IsNotNull(service);
}

L'integrazione crea un ambito di dependency injection per ogni invocazione di test, inclusa ogni riga di dati e ogni tentativo di ripetizione. TestInitialize, il metodo di test e TestCleanup condividono la stessa istanza e ambito della classe di test. Dopo la pulizia, MSTest rilascia l'istanza della classe di test e quindi l'ambito. L'applicazione è ancora proprietaria del provider host e del servizio radice.

Questa prima integrazione basata sulla reflection non supporta Native AOT, WebAssembly del browser, la compilazione AOT o la generazione del codice sorgente di MSTest. L'API AddMSTestTestClassInjection usa l'ID MSTESTEXP di diagnostica e potrebbe cambiare in una versione futura. Per la configurazione dell'host, vedere Microsoft. Integrazione delle estensioni.

Suggerimento

Analizzatori correlati: MSTEST0063 convalida i costruttori per l'iniezione gestita dall'host e MSTEST0088 segnala le modalità di compilazione non supportate.

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()
    {
        int 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()
    {
        HttpResponseMessage 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 hai abilitato analizzatori diversi da MSTest, come ad esempio le regole di analisi del codice .NET, potresti vedere CA1001 che suggerisce di implementare il pattern di dispose quando la tua classe di test possiede 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