Condividi tramite


Scrivere unit test per DLL C++

Questa procedura dettagliata descrive come sviluppare una DLL C++ nativa usando la metodologia dei test preventivi. I passaggi di base sono i seguenti:

  1. Creare un progetto di test nativo. Il progetto di test si trova nella stessa soluzione del progetto DLL.

  2. Creare un progetto DLL. In questa procedura dettagliata viene creato un nuovo file DLL, ma la procedura per testare una DLL esistente è simile.

  3. Definire le funzioni DLL visibili ai test.

  4. Aumentare i test iterativamente. Si consiglia un ciclo "red-green-refactor", in cui lo sviluppo di codice è condotto dai test.

  5. Debug di test non superati. È possibile eseguire test in modalità debug.

  6. Effettuare il refactoring mantenendo i test invariati. Il refactoring indica il miglioramento della struttura del codice senza modificarne il comportamento esterno. È possibile eseguirla per migliorare prestazioni, estendibilità o leggibilità del codice. Poiché lo scopo non è di modificare il comportamento, non si modificano i test mentre si esegue il refactoring del codice. I test consentono di verificare che non vengano inseriti bug durante il refactoring.

  7. Controllo del code coverage. I test d'unità sono particolarmente utili quando eseguono più parti del codice. È possibile individuare quali parti del codice sono state usate dai test.

  8. Unità isolate da risorse esterne. In genere, una DLL dipende da altri componenti del sistema che si sta sviluppando, come altre DLL, i database, o dei sottosistemi remoti. È utile verificare ogni unità in isolamento dalle relative dipendenze. I componenti esterni possono provocare un rallentamento dell'esecuzione dei test. Durante lo sviluppo, gli altri componenti potrebbero non essere completati.

Creare un progetto nativo di unit test

  1. Nel menu File scegliere Nuovo>Progetto.

    Visual Studio 2017 e versioni precedenti: espandere Modelli>installati>Visual C++>Test. Visual Studio 2019: impostare Linguaggio su C++ e digitare "test" nella casella di ricerca.

    Scegliere il modello Progetto unit test nativo o scegliere un qualsiasi altro framework installato. Se si sceglie un altro modello, ad esempio Google Test o Boost.Test, i principi di base sono gli stessi, cambiano però alcuni dettagli.

    In questa procedura dettagliata, il progetto di test viene denominato NativeRooterTest.

  2. Nel nuovo progetto, controllare unittest1.cpp

    Test project with TEST_CLASS and TEST_METHOD

    Si noti che:

    • Ogni test è definito tramite TEST_METHOD(YourTestName){...}.

      Non è necessario scrivere una firma della funzione formale. La firma viene creata dalla macro TEST_METHOD. La macro genera un'istanza a una funzione che restituisce un valore nullo. Viene inoltre generata una funzione statica che restituisce informazioni sul metodo di test. Queste informazioni consentono ad Esplora test di individuare il metodo.

    • I metodi dei test vengono raggruppati in classi usando TEST_CLASS(YourClassName){...}.

      Quando vengono eseguiti i test, viene creata un'istanza di ogni classe di test. I metodi di test vengono chiamati in un ordine non specificato. È possibile definire metodi speciali che vengono richiamati prima e dopo ogni modulo, classe, o metodo.

  3. Verificare che i test vengano eseguiti in Esplora test:

    1. Inserire il codice di test:

      TEST_METHOD(TestMethod1)
      {
          Assert::AreEqual(1,1);
      }
      

      Si noti che la classe Assert fornisce diversi metodi statici che è possibile usare per verificare i risultati nei metodi di test.

    2. Nel menu Test scegliere Esegui>Tutti i test.

      Viene eseguita la compilazione e l'esecuzione del test.

      Verrà visualizzato Esplora test.

      Il test verrà visualizzato in Test superati.

      Unit Test Explorer with one passed test

Creare un progetto DLL

La procedura seguente illustra come creare un progetto DLL in Visual Studio 2019.

  1. Creare un progetto C++ usando la Creazione guidata desktop di Windows: fare clic con il pulsante destro del mouse sul nome della soluzione in Esplora soluzioni e scegliere Aggiungi>nuovo progetto. Impostare Linguaggio su C++ e quindi digitare "windows" nella casella di ricerca. Scegliere Creazione guidata applicazione desktop di Windows dall'elenco risultati.

    In questa procedura dettagliata, il progetto viene denominato RootFinder.

  2. Premere Crea. Nella finestra di dialogo successiva, in Tipo di applicazione scegliere DLL (libreria a collegamento dinamico) e selezionare Esporta simboli.

    L'opzione Esporta simboli genera una semplice macro che è possibile usare per dichiarare i metodi esportati.

    C++ project wizard set for DLL and Export Symbols

  3. Dichiarare una funzione esportata nel file con estensione h principale:

    New DLL code project and .h file with API macros

    Il dichiaratore __declspec(dllexport) permette ai membri public e protected della classe di essere visibili al di fuori della DLL. Per altre informazioni, vedere Using dllimport and dllexport in C++ Classes.

  4. Nel file con estensione cpp principale, aggiungere il corpo minimo della funzione:

        // Find the square root of a number.
        double CRootFinder::SquareRoot(double v)
        {
            return 0.0;
        }
    

Accoppiare il progetto di test al progetto DLL

  1. Aggiungere il progetto DLL ai riferimenti del progetto di test:

    1. Fare clic con il pulsante destro del mouse sul nodo del progetto in Esplora soluzioni e scegliere Aggiungi>Riferimento.

    2. Nella finestra di dialogo Aggiungi riferimento , selezionare il progetto DLL e scegliere Aggiungi.

      C++ project properties | Add New Reference

  2. Nel file principale con estensione cpp dello unit test, includere il file con estensione h del codice DLL:

    #include "..\RootFinder\RootFinder.h"
    
  3. Aggiungere un test di base che usa la funzione esportata:

    TEST_METHOD(BasicTest)
    {
       CRootFinder rooter;
       Assert::AreEqual(
          // Expected value:
          0.0,
          // Actual value:
          rooter.SquareRoot(0.0),
          // Tolerance:
          0.01,
         // Message:
         L"Basic test failed",
         // Line number - used if there is no PDB file:
         LINE_INFO());
    }
    
  4. Compilare la soluzione.

    Il nuovo test viene visualizzato in Esplora test.

  5. In Esplora test scegliere Esegui tutto.

    Unit Test Explorer - Basic Test passed

    È stato installato il test e i progetti di codice, e verificato che sia possibile eseguire test che eseguono funzioni nel progetto di codice. Ora è possibile iniziare a scrivere test e codici reali.

Incrementare i test in maniera iterativa e fare in modo che siano superati

  1. Aggiungere un nuovo test:

    TEST_METHOD(RangeTest)
    {
      CRootFinder rooter;
      for (double v = 1e-6; v < 1e6; v = v * 3.2)
      {
        double actual = rooter.SquareRoot(v*v);
        Assert::AreEqual(v, actual, v/1000);
      }
    }
    

    Suggerimento

    È consigliabile non modificare i test che siano stati superati. Al contrario, aggiungere un nuovo test, aggiornare il codice in modo che il test passi e quindi aggiungere un altro test, e così via.

    Quando gli utenti modificano i requisiti, disabilitare i test che non sono più corretti. Scrivere nuovi test e farli funzionare uno alla volta, nello stesso modo incrementale.

  2. Compilare la soluzione e quindi scegliere Esegui tutto in Esplora test.

    Il nuovo test non riesce.

    The RangeTest fails

    Suggerimento

    Verificare che ogni test non venga superato subito dopo averlo scritto. Questo consente di evitare il semplice errore di scrivere un test che riesce sempre.

  3. Ottimizzare il codice della DLL in modo che il nuovo test abbia esito positivo:

    #include <math.h>
    ...
    double CRootFinder::SquareRoot(double v)
    {
      double result = v;
      double diff = v;
      while (diff > result/1000)
      {
        double oldResult = result;
        result = result - (result*result - v)/(2*result);
        diff = abs (oldResult - result);
      }
      return result;
    }
    
  4. Compilare la soluzione e quindi scegliere Esegui tutto in Esplora test.

    Entrambi i test vengono superati.

    Unit Test Explorer - Range Test passed

    Suggerimento

    Sviluppare il codice aggiungendo un test alla volta. Assicurarsi che tutti i test vengano superati dopo ogni iterazione.

Eseguire il debug di un test non superato

  1. Aggiungere un altro test:

    #include <stdexcept>
    ...
    // Verify that negative inputs throw an exception.
    TEST_METHOD(NegativeRangeTest)
    {
      wchar_t message[200];
      CRootFinder rooter;
      for (double v = -0.1; v > -3.0; v = v - 0.5)
      {
        try
        {
          // Should raise an exception:
          double result = rooter.SquareRoot(v);
    
          _swprintf(message, L"No exception for input %g", v);
          Assert::Fail(message, LINE_INFO());
        }
        catch (std::out_of_range ex)
        {
          continue; // Correct exception.
        }
        catch (...)
        {
          _swprintf(message, L"Incorrect exception for %g", v);
          Assert::Fail(message, LINE_INFO());
        }
      }
    }
    
  2. Compilare la soluzione e scegliere Esegui tutto.

  3. Aprire (o fare doppio clic) sul test non superato.

    L'asserzione fallita viene evidenziata. Il messaggio di errore è visibile nel riquadro dei dettagli di Esplora test.

    NegativeRangeTests failed

  4. Per capire perché il test non riesce, scorrere la funzione:

    1. Impostare il punto di interruzione all'inizio della funzione SquareRoot.

    2. Dal menu di scelta rapida del test non superato, scegliere Esegui debug test selezionati.

      Quando l'esecuzione si arresta in corrispondenza del punto di interruzione, eseguire il codice un'istruzione alla volta.

  5. Inserire il codice nella funzione che si sta sviluppando:

    
    #include <stdexcept>
    ...
    double CRootFinder::SquareRoot(double v)
    {
        // Validate parameter:
        if (v < 0.0)
        {
          throw std::out_of_range("Can't do square roots of negatives");
        }
    
    
  6. Tutti i test vengono ora superati.

    All tests pass

Suggerimento

Se i singoli test non hanno dipendenze che ne impediscono l'esecuzione in qualsiasi ordine, attivare l'esecuzione parallela dei test nel menu Impostazioni sulla barra degli strumenti. Questo può ridurre notevolmente il tempo impiegato per eseguire tutti i test.

Effettuare il refactoring del codice senza modificare i test

  1. Semplificare il calcolo centrale nella funzione SquareRoot:

    // old code:
    //   result = result - (result*result - v)/(2*result);
    // new code:
         result = (result + v/result)/2.0;
    
    
  2. Compilare la soluzione e scegliere Esegui tutto, per assicurarsi che non sia stato introdotto un errore.

    Suggerimento

    Un buon set di test d'unità consente di assicurarsi che non siano stati introdotti bug con la modifica del codice.

    Mantenere il refactoring separato da altre modifiche.

Passaggi successivi

  • Isolamento. La maggior parte delle DLL dipende da altri sottosistemi quali database ed altre DLL. Queste altre componenti vengono spesso sviluppate in parallelo. Per permettere l'esecuzione di unit test mentre gli altri componenti non sono ancora disponibili è necessario sostituirli con una simulazione.

  • Test di verifica della compilazione. È possibile eseguire test sul server di compilazione del team a intervalli predefiniti. Questo assicura che i bug non verranno introdotti quando il lavoro dei diversi membri del team viene integrato.

  • Test di controllo. È possibile lasciare che alcuni test vengano eseguiti prima che ogni membro del team esegua controllo del codice sorgente. In genere questo è un sottoinsieme del set completo dei test di verifica della compilazione.

    È anche possibile lasciare al chiamante un livello minimo di code coverage.