Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Sviluppo e test di software Agile C++ con Visual Studio e TFS
John Socha-Leialoha
Vorrei iniziare con una domanda destinata agli sviluppatori di un'applicazione compilata in Visual C++: non sarebbe ideale poter aumentare il livello di produttività, realizzare codice di qualità superiore e riscriverlo in base alle esigenze per migliorare l'architettura senza alcun rischio? E ora una domanda per i tester: non sarebbe opportuno trascorrere meno tempo a scrivere e gestire test, per potersi dedicare ad altre attività relative al testing?
In questo articolo illustrerò una serie di tecniche utilizzate dal nostro team in Microsoft per compilare applicazioni.
Il nostro team è piuttosto ridotto. 10 persone lavorano a tre progetti diversi contemporaneamente. Questi progetti sono scritti in C# e C++. Il codice C++ viene impiegato soprattutto per i programmi da eseguire in Windows PE, una versione ridotta di Windows spesso utilizzata per le installazioni del sistema operativo. Viene inoltre adottato nell'ambito di una sequenza di attività di Microsoft System Center Configuration Manager per eseguire attività nell'intero sistema operativo, ad esempio l'acquisizione di un disco rigido in un file disco rigido virtuale (VHD). Un notevole impegno per un team di piccole dimensioni, che pertanto deve assolutamente essere produttivo.
Il nostro team utilizza Visual Studio 2010 e Team Foundation Server (TFS) 2010. Utilizziamo TFS 2010 per il controllo delle versioni, il monitoraggio del lavoro, l'integrazione continua, la
Quando e perché il nostro team scrive test
Inizierò esaminando i motivi per cui il nostro team scrive test (le risposte possono variare da team a team). La risposta specifica è leggermente diversa per sviluppatori e tester, ma alla fine nemmeno più di tanto. Ecco i miei obiettivi di sviluppatore:
- Nessuna interruzione di generazione
- Nessuna regressione
- Refactoring in tutta sicurezza
- Modifica dell'architettura in tutta sicurezza
- Progettazione mediante sviluppo basato su test
Ovviamente la qualità è il motivo principale alla base di questi obiettivi. Una volta soddisfatti questi obiettivi, la vita di uno sviluppatore diviene decisamente più produttiva e divertente.
Passerò ora a concentrarmi su un aspetto di un tester Agile: la scrittura di test automatizzati. Gli obiettivi dei nostri tester in fase di scrittura di test automatizzati includono assenza di regressioni, sviluppo basato su accettazioni e raccolta e segnalazione di code coverage.
Naturalmente i nostri tester non si limitano a scrivere test automatizzati, ma sono anche responsabili della raccolta di code coverage, in quanto desideriamo che i numeri di code coverage includano i risultati di tutti i test e non solo degli unit test (maggior informazioni più avanti).
In questo articolo illustrerò diversi strumenti e tecniche impiegati dal nostro team per raggiungere gli obiettivi qui delineati.
Eliminazione di interruzioni di generazione con archiviazioni gestite
In passato il nostro team utilizzava rami per garantire ai tester una generazione sempre stabile. La manuntenzione dei rami prevede tuttavia un sovraccarico associato. Grazie alle archiviazioni gestite utilizziamo ora le diramazioni solo per i rilasci: una svolta notevole.
L'utilizzo di archiviazioni gestite prevede la configurazione del controllo delle compilazioni e di uno o più agenti di compilazione. Questo argomento non verrà affrontato nell'articolo, ma sono presenti maggiori dettagli nella pagina MSDN Library "Amministrazione di Team Foundation Build" all'indirizzo bit.ly/jzA8Ff.
Dopo aver configurato gli agenti di compilazione, è possibile creare una nuova definizione di compilazione per le archiviazioni gestite attenendosi alla procedura indicata di seguito in Visual Studio:
Fare clic su Visualizza nella barra dei menu e quindi su Team Explorer per verificare che la finestra dello strumento Team Explorer sia visibile.
Espandere il progetto del team e fare clic con il pulsante destro del mouse su Compila.
Fare clic su Nuova definizione di compilazione.
Fare clic su Trigger a sinistra e selezionare Archiviazione gestita, come illustrato nella Figura 1.
Figura 1 Selezionare l'opzione Archiviazione gestita per Nuova definizione di compilazione
Fare clic su Impostazioni predefinite compilazione e selezionare il controller di compilazione.
Fare clic su Elaborazione e selezionare gli elementi da compilare.
Una volta salvata questa definizione di compilazione (noi l'abbiamo denominata "Archiviazione gestita") verrà visualizzata una nuova finestra di dialogo dopo aver inviato l'archiviazione (vedere la Figura 2). Fare clic su Modifiche alla compilazione per creare un'area di sospensione e inviarla al server di compilazione. Se non sono presenti errori di compilazione e gli unit test hanno esito positivo, in TFS le modifiche verranno automaticamente archiviate. In caso contrario l'archiviazione verrà rifiutata.
Figura 2 Finestra di dialogo Archiviazione gestita
Le archiviazioni gestite sono decisamente utili, in quanto garantiscono l'assenza di interruzioni di generazione e l'esito positivo di tutti gli unit test. È fin troppo scontato per uno sviluppatore dimenticarsi di eseguire tutti i test prima dell'archiviazione, ma con le archiviazioni gestite il problema non sussiste più.
Scrittura di unit test C++
Dopo aver appreso come eseguire gli unit test nell'ambito di un'archiviazione gestita, vediamo in che modo è possibile scrivere unit test per codice C++ nativo.
Sono un grande appassionato dello sviluppo basato su test per molte ragioni. Mi consente di concentrarmi sul comportamento e semplificare le progettazioni. Un'alternativa valida è rappresentata da una serie di test che definiscono il contratto comportamentale. Posso eseguire il refactoring senza il rischio di introdurre bug derivanti da una violazione accidentale del contratto. E so per certo che alcuni sviluppatori non violerebbero un comportamento richiesto di cui non sono a conoscenza.
Uno sviluppatore del team era in grado di utilizzare il programma Test Runner (mstest) integrato per testare codice C++. Scriveva unit test Microsoft .NET Framework mediante codice C++/CLI che chiama funzioni pubbliche esposte da una DLL nativa in C++. In questa sezione analizzerò più nel dettaglio questo approccio per avviare direttamente un'istanza di classi C++ native interne al codice produzione. In altre parole è possibile non limitarsi al testing dell'interfaccia pubblica.
La soluzione consiste nell'inserire il codice di produzione in una libreria statica che può essere collegata alle DLL dello unit test, nonché all'EXE o alla DLL di produzione, come illustrata nella Figura 3.
Figura 3 I test e il prodotto condividono lo stesso codice tramite una libreria statica
Di seguito sono indicati i passaggi necessari per configurare i progetti seguendo questa procedura. Iniziare creando la libreria statica:
- In Visual Studio fare clic su File, Nuovo e Progetto.
- Fare clic su Visual C++ nell'elenco Modelli installati (è necessario espandere Altri linguaggi).
- Fare clic su Progetto Win32 nell'elenco dei tipi di progetto.
- Immettere il nome del progetto e scegliere OK.
- Fare clic su Avanti, Libreria statica e quindi su Fine.
Creare ora la DLL di test. La configurazione di un progetto di test richiede ancora alcuni passaggi. È necessario creare il progetto, ma anche garantirne l'accesso al codice e ai file di intestazione nella libreria statica.
Fare clic con il pulsante destro del mouse sulla soluzione nella finestra Esplora soluzioni. Fare clic su Aggiungi, quindi scegliere Nuovo progetto. Fare clic su Test nel nodo Visual C++ nell'elenco di modelli. Digitare il nome del progetto (il nostro team aggiunge UnitTests alla fine del nome del progetto) e fare clic su OK.
Fare clic con il pulsante destro del mouse sul nuovo progetto in Esplora soluzioni e scegliere Proprietà. Fare clic su Proprietà comuni nella struttura a sinistra. Scegliere Aggiungi nuovo riferimento. Fare clic sulla scheda Progetti, selezionare il progetto con la libreria statica e scegliere OK per chiudere la finestra di dialogo Aggiungi riferimento.
Espandere il nodo Proprietà di configurazione nella struttura a sinistra, quindi espandere il nodo C/C++. Fare clic su Generale nel nodo C/C++. Fare clic sulla casella combinata Configurazione e selezionare Tutte le configurazioni per verificare di modificare le versioni Debug e Rilascio.
Fare clic su Includi librerie aggiuntive e immettere un percorso per la libreria statica, in cui sarà necessario sostituire il nome della libreria statica di MyStaticLib:
$(SolutionDir)\MyStaticLib;%(AdditionalIncludeDirectories)
Fare clic sulla proprietà Supporto Common Language Runtime nello stesso elenco di proprietà e modificarla in Supporto Common Language Runtime (/clr).
Fare clic sulla sezione Generale in Proprietà di configurazione e modificare la proprietà TargetName in $(ProjectName). Per impostazione predefinita la proprietà è configurata su DefaultTest per tutti i progetti di test, ma dovrebbe corrispondere al nome del progetto. Fare clic su OK.
È opportuno ripetere la prima parte della procedura per aggiungere la libreria statica all'EXE o alla DLL di produzione.
Scrittura del primo unit test
A questo punto è possibile scrivere un nuovo unit test. I metodi di test saranno metodi .NET scritti in C++ e pertanto la sintassi sarà leggermente diversa da quella del codice C++ nativo. Per chi conosce C#, si tratta in un certo senso di un mix tra la sintassi di C++ e C#. Per maggiori dettagli consultare la documentazione MSDN Library "Caratteristiche di linguaggio per il CLR di destinazione" all'indirizzo bit.ly/iOKbR0.
Supponiamo di disporre di una definizione di classe da testare simile alla seguente:
#pragma once
class MyClass {
public:
MyClass(void);
~MyClass(void);
int SomeValue(int input);
};
Ora desideriamo scrivere un test affinché il metodo SomeValue specifichi il comportamento per questo metodo. Nella Figura 4 è illustrato un esempio di unit test semplice, visualizzando l'intero file con estensione cpp.
Figura 4 Unit test semplice
#include "stdafx.h"
#include "MyClass.h"
#include <memory>
using namespace System;
using namespace Microsoft::VisualStudio::TestTools::UnitTesting;
namespace MyCodeTests {
[TestClass]
public ref class MyClassFixture {
public:
[TestMethod]
void ShouldReturnOne_WhenSomeValue_GivenZero() {
// Arrange
std::unique_ptr<MyClass> pSomething(new MyClass);
// Act
int actual = pSomething->SomeValue(0);
// Assert
Assert::AreEqual<int>(1, actual);
}
};
}
Per chi non è pratico di scrittura di unit test, utilizzerò un modello noto come Arrange, Act, Assert. La parte Arrange configura le precondizioni per lo scenario che si desidera testare. Act è la sezione in cui viene chiamato il metodo da testare. mentre Assert è quella in cui viene controllato che il metodo abbia seguito il comportamento desiderato. Per una maggiore leggibilità e semplificare la ricerca della sezione Act aggiungerò un commento prima di ogni sezione.
I metodi di test sono contrassegnati dall'attributo TestMethod, come illustrato nella Figura 4. Questi metodi a loro volta devono essere inclusi in una classe contrassegnata con l'attributo TestClass.
La prima riga del metodo di test crea una nuova istanza della classe C++ nativa. Utilizzo la classe unique_ptr standard della libreria C++ per verificare che quest'istanza verrà automaticamente eliminata alla fine del metodo di test. Sarà pertanto evidente la possibilità di combinare codice C++ nativo con codice .NET CLI/C++. Sussistono ovviamente alcune restrizioni, che illustrerò nella sezione indicata di seguito.
Come già indicato in precedenza, per chi non ha mai scritto test .NET, la classe Assert presenta diversi metodi che è possibile utilizzare per controllare varie condizioni. Utilizzo la versione generica per specificare in modo esplicito il tipo di dati previsto dal risultato.
Il massimo delle potenzialità dei test C++/CLI
Sussistono dunque alcune limitazioni da non trascurare in caso di combinazione di codice C++ nativo e codice C++/CLI. Le differenze rappresentano il risultato della differenza nella gestione della memoria tra i due codebase. Il codice C++ nativo utilizza un nuovo operatore C++ per allocare la memoria e l'utente dovrà occuparsi di liberare la memoria. Una volta allocata una parte di memoria, i dati si troveranno sempre nello stesso punto.
D'altro canto, i puntatori nel codice C++/CLI presentano un comportamento diverso a causa del modello di Garbage Collection ereditato da .NET Framework. È possibile creare nuovi oggetti .NET in C++/CLI mediante l'operatore gcnew anziché il nuovo operatore, che restituisce un handle di oggetto e non un puntatore all'oggetto. Gli handle sono puntatori di base a un puntatore. Se la Garbage Collection sposta gli oggetti gestiti all'interno della memoria, gli handle vengono aggiornati al nuovo percorso.
In caso di combinazione di puntatori gestiti e nativi, è necessario prestare la massima attenzione. Nell'articolo analizzerò alcune di queste differenze e fornirò suggerimenti per sfruttare al meglio i test C++/CLI per oggetti C++ nativi.
Supponiamo di disporre di un metodo da testare che restituisce un puntatore a una stringa. In C++ è possibile rappresentare il puntatore di stringa con LPCTSTR, ma una stringa .NET è rappresentata da String^ in C++/CLI. L'accento circonflesso dopo il nome della classe rappresenta un handle a un oggetto gestito.
Ecco un esempio per testare il valore di una stringa restituito da una chiamata al metodo:
// Act
LPCTSTR actual = pSomething->GetString(1);
// Assert
Assert::AreEqual<String^>("Test", gcnew String(actual));
L'ultima riga contiene tutti i dettagli. È presente un metodo AreEqual che accetta stringhe gestite, ma non un metodo corrispondente per le stringhe C++ native. Sarà quindi necessario utilizzare stringhe gestite. Il primo parametro per il metodo AreEqual è una stringa gestita e pertanto si tratta di una stringa Unicode, anche se non è contrassegnata come stringa Unicode mediante, ad esempio _T o L.
La classe String presenta un costruttore che accetta una stringa C++ ed è pertanto possibile creare una nuova stringa gestita che conterrà il valore effettivo ricavato dal metodo in fase di testing e a quel punto AreEqual garantisce che abbiano lo stesso valore.
La classe Assert offre due metodi particolarmente interessanti: IsNull e IsNotNull. Il parametro per questi metodi è tuttavia un handle, non un puntatore all'oggetto, il che significa che è possibile utilizzarli solo con oggetti gestiti. In alternativa, è possibile utilizzare il metodo IsTrue:
Assert::IsTrue(pSomething != nullptr, "Should not be null");
Il risultato è lo stesso, ma con una porzione di codice in più. Aggiungo un commento per chiarire meglio l'aspettativa nel messaggio visualizzato nella finestra dei risultati del test, come indicato nella Figura 5.
Figura 5 Risultati del test che indicano il commento aggiuntivo nel messaggio di errore
Condivisione di codice di impostazione e teardown
Il codice di test deve essere considerato come codice di produzione. In altre parole è necessario eseguire il refactoring dei test nella stessa misura del codice di produzione per poter semplificare la gestione del codice di test. In alcuni casi potrebbero coesistere porzioni di codice di impostazione e teardown comuni per tutti i metodi di test in una classe di test. È possibile designare un metodo che verrà eseguito prima di ogni test e un metodo da eseguire successivamente al test, ma non entrambi.
L'attributo TestInitialize contrassegna un metodo che verrà eseguito prima di ogni metodo di test nella classe di test e in modo analogo l'attributo TestCleanup contrassegna un metodo che verrà eseguito dopo ogni metodo di test nella classe di test. Ecco un esempio:
[TestInitialize]
void Initialize() {
m_pInstance = new MyClass;
}
[TestCleanup]
void Cleanup() {
delete m_pInstance;
}
MyClass *m_pInstance;
Come risulta evidente, ho utilizzato un semplice puntatore alla classe per m_pInstance. Per quale motivo allora non è possibile utilizzare unique_ptr per gestire la durata?
La risposta, ancora una volta, ha a che fare con la combinazione di codice C++ nativo e C++/CLI. Le variabili di istanze in C++/CLI fanno parte di un oggetto gestito e possono pertanto essere handle per un oggetto gestito, puntatori a oggetti nativi o tipi di valore. È necessario tornare ai concetti di base di creazione ed eliminazione per gestire la durata delle istanze del codice C++ nativo.
Utilizzo di puntatori a variabili di istanze
In caso di utilizzo di COM è possibile trovarsi nella situazione di dover scrivere quanto indicato nell'esempio seguente:
[TestMethod]
Void Test() {
...
HRESULT hr = pSomething->GetWidget(&m_pUnk);
...
}
IUnknown *m_pUnk;
La compilazione avrà esito negativo e verrà generato un messaggio di errore:
impossibile convertire il parametro 1 da 'cli::interior_ptr<Type>' a 'IUnknown **'
L'indirizzo di una variabile di istanza C++/CLI presenta in questo caso il tipo interior_ptr<IUnknown *>, ovvero un tipo incompatibile con codice nativo C++. Per quale motivo? Mi serviva un puntatore.
La classe di test è una classe gestita ed è pertanto possibile spostare le istanze di questa classe nella memoria tramite Garbage Collector. Se dunque fosse presente un puntatore a una variabile di istanza e l'oggetto venisse spostato, il puntatore non sarebbe più valido.
È possibile bloccare l'oggetto per la durata della chiamata nativa nel modo seguente:
cli::pin_ptr<IUnknown *> ppUnk = &m_pUnk;
HRESULT hr = pSomething->GetWidget(ppUnk);
La prima riga blocca l'istanza finché la variabile non esula dall'ambito. A quel punto sarà possibile passare un puntatore alla variabile di istanza a un codice C++ nativo, sebbene la variabile sia contenuta all'interno di una classe di test gestita.
Scrittura di codice verificabile
All'inizio di questo articolo ho sottolineato l'importanza di scrivere codice verificabile. Utilizzo lo sviluppo basato su test per essere certo che il mio codice sia verificabile, ma alcuni sviluppatori preferiscono scrivere test subito dopo aver scritto il codice. In ogni caso, è importante non pensare solo agli unit test, ma all'intero stack di test.
Mike Cohn, un autore popolare e prolifico di testi su Agile, ha realizzato una piramide per l'automazione di test che offre un'idea dei tipi di test e del numero di test ideale a ogni livello. Gli sviluppatori dovrebbero scrivere tutti gli unit test e i test di componenti (o almeno gran parte di essi), nonché alcuni test di integrazione. Per maggiori dettagli su questa piramide di testing, leggere il post di blog di Cohn "Il livello dimenticato della piramide per l'automazione dei test" (bit.ly/eRZU2p).
I tester sono in genere responsabili della scrittura di test di accettazione e dell'interfaccia utente. Talvolta questi test vengono chiamati "end-to-end" o E2E. Nella piramide di Cohn il triangolo dell'interfaccia utente è minuscolo rispetto alle aree degli altri tipi di test. L'idea è quella di scrivere un numero minore possibile di test di interfaccia utente automatizzati. Questi test tendono a essere fragili e dispensiosi da scrivere e mantenere. Una minima modifica all'interfaccia utente può facilmente determinare l'interruzione dei test dell'interfaccia utente.
Se il codice non viene scritto per essere verificabile, la piramide potrebbe risultare invertita e la maggior parte dei test automatizzati sarà costituita dai test dell'interfaccia utente. Si tratta di una situazione sfavorevole, ma la verità è che spetta allo sviluppatore garantire che i tester possano scrivere test di integrazione e accettazione al di sotto dell'interfaccia utente.
Inoltre, per una qualche ragione, la maggior parte dei tester in cui mi sono imbattuto preferisce di gran lunga scrivere test in C#, rispetto a C++. Di conseguenza il nostro team aveva bisogno di un bridge tra il codice C++ sottoposto a test e i test automatizzati. Il bridge assume la forma degli strumenti di test, ovvero classi C++/CLI visualizzate dal codice C# come qualsiasi altra classe gestita.
Compilazione di strumenti di test da C# a C++
Queste tecniche non variano molto da quelle trattate per la scrittura di test C++/CLI. In entrambe viene utilizzato lo stesso tipo di codice in modalità mista: la differenza è rappresentata dal modo in cui vengono impiegate.
Il primo passaggio consiste nel creare un nuovo progetto che conterrà gli strumenti di test:
- Fare clic con il pulsante destro del mouse sul nodo della soluzione in Esplora soluzioni, scegliere Aggiungi e quindi Nuovo progetto.
- In Altri linguaggi, Visual C++, CLR, fare clic su Libreria di classi.
- Immettere il nome da utilizzare per il progetto e scegliere OK.
- Ripetere i passaggi per la creazione di un progetto di test al fine di aggiungere un riferimento e includere i file.
La classe stessa dello strumento di test risulterà analoga alla classe di test, ma senza i vari attributi (vedere la Figura 6).
Figura 6 Strumento di test da C# a C++
#include "stdafx.h"
#include "MyClass.h"
using namespace System;
namespace MyCodeFixtures {
public ref class MyCodeFixture {
public:
MyCodeFixture() {
m_pInstance = new MyClass;
}
~MyCodeFixture() {
delete m_pInstance;
}
!MyCodeFixture() {
delete m_pInstance;
}
int DoSomething(int val) {
return m_pInstance->SomeValue(val);
}
MyClass *m_pInstance;
};
}
Come si può notare, non è presente il file di intestazione. Questa è una delle mie funzionalità preferite di C++/CLI. Poiché la libreria di classi compila un assembly gestito, le informazioni sulle classi vengono archiviate come informazioni di tipo .NET e pertanto non sono necessari file di intestazione.
Questa classe contiene inoltre un distruttore e un finalizzatore. Non si tratta effettivamente di un distruttore: il compilatore riscrive il distruttore in un'implementazione del metodo Dispose nell'interfaccia IDisposable. Qualsiasi classe C++/CLI che dispone di un distruttore implementa dunque l'interfaccia IDisposable.
Il metodo !MyCodeFixture è il finalizzatore, chiamato dal Garbage Collector quando decide di liberare questo oggetto, a meno che non sia stato precedentemente chiamato il metodo Dispose. È possibile impiegare l'istruzione using per controllare la durata dell'oggetto C++ nativo incorporato oppure gestirla tramite il Garbage Collector. Per maggiori dettagli su questo comportamento, leggere l'articolo MSDN Library "Modifiche nella semantica del distruttore" all'indirizzo bit.ly/kW8knr.
Se si dispone di una classe dello strumento di test C++/CLI, è possibile scrivere uno unit test C# analogo a quello illustrato nella Figura 7.
Figura 7 Sistema di unit test C#
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MyCodeFixtures;
namespace MyCodeTests2 {
[TestClass]
public class UnitTest1 {
[TestMethod]
public void TestMethod1() {
// Arrange
using (MyCodeFixture fixture = new MyCodeFixture()) {
// Act
int result = fixture.DoSomething(1);
// Assert
Assert.AreEqual<int>(1, result);
}
}
}
}
Preferisco impiegare un'istruzione using per controllare in modo esplicito la durata dell'oggetto strumento di test, anziché utilizzare il Garbage Collector. Questa condizione è estremamente importante nei metodi di test per garantire che i test non interagiscano tra loro.
Acquisizione e segnalazione di code coverage
L'ultimo elemento che ho citato all'inizio dell'articolo è il code coverage. L'obiettivo del mio team consiste nel garantire che il code coverage venga automaticamente acquisito dal server di compilazione, venga pubblicato in TFS e sia sempre disponibile per chiunque.
Per prima cosa ho dovuto capire come acquisire il code coverage C++ dai test in esecuzione. Navigando nel Web ho trovato un post di blog informativo di Emil Gustafsson, intitolato "Report di code coverage C++ nativo mediante Visual Studio 2008 Team System" (bit.ly/eJ5cqv). In questo post vengono delineati i passaggi necessari per acquisire informazioni sul code coverage. Ho trasformato il risultato in un file CMD eseguibile in qualsiasi momento nel computer di sviluppo per acquisire le informazioni sul code coverage:
"%VSINSTALLDIR%\Team Tools\Performance Tools\vsinstr.exe" Tests.dll /COVERAGE
"%VSINSTALLDIR%\Team Tools\Performance Tools\vsperfcmd.exe" /START:COVERAGE /WaitStart /OUTPUT:coverage
mstest /testcontainer:Tests.dll /resultsfile:Results.trx
"%VSINSTALLDIR%\Team Tools\Performance Tools\vsperfcmd.exe" /SHUTDOWN
Sostituire Tests.dll con il nome effettivo della DLL contenente test. Sarà inoltre necessario preparare l'instrumentazione delle DLL:
- Fare clic con il pulsante destro del mouse sul progetto di test nella finestra Esplora soluzioni.
- Scegliere Proprietà.
- Selezionare la configurazione Debug.
- Espandere Proprietà di configurazione e Linker, quindi fare clic su Avanzate.
- Modificare la proprietà Profile su Yes (/PROFILE).
- Fare clic su OK.
Questi passaggi abilitano il profiling, che dovrà essere attivato per instrumentare gli assembly al fine di acquisire informazioni sul code coverage.
Ricompilare il progetto ed eseguire il file CMD. Verrà creato un file di code coverage. Caricare il file in Visual Studio per poter essere in grado di acquisire il code coverage dai test.
L'esecuzione di questi passaggi nel server di compilazione e la pubblicazione dei risultati in TFS richiedono un modello di compilazione personalizzato. I modelli di compilazione TFS vengono archiviati nel controllo delle versioni e appartengono a uno specifico progetto di team. Una cartella denominata BuildProcessTemplates in ogni progetto di team conterrà probabilmente diversi modelli di compilazione.
Per utilizzare il modello di compilazione personalizzato nel download, aprire la finestra Esplora controllo codice sorgente. Passare alla cartella BuildProcessTemplates nel progetto del team e verificare di averne eseguito il mapping a una directory nel computer. Copiare il file BuildCCTemplate.xaml in questo percorso di mapping. Aggiungere il modello al controllo del codice sorgente e archiviarlo.
È necessario archiviare i file dei modelli prima di poterli utilizzare nelle definizioni di compilazione.
Dopo aver archiviato il modello di compilazione, sarà possibile creare una definizione di compilazione per l'esecuzione del code coverage. Il code coverage C++ viene raccolto mediante il comando vsperfmd, come già illustrato. Vsperfmd esegue il listening delle informazioni sul code coverage per tutti i file eseguibili instrumentati eseguiti contemporaneamente a vsperfcmd. Non è pertanto opportuno disporre di altri test instrumentati contemporaneamente in esecuzione. Verificare inoltre che nel computer che elaborerà queste esecuzioni di code coverage venga eseguito un solo agente di compilazione.
Ho creato una definizione di build eseguibile in orari notturni. A tale scopo, attenersi alla procedura seguente:
- Nella finestra Esplora risorse espandere il nodo per il progetto del team.
- Fare clic con il pulsante destro del mouse su Compilazioni, un nodo del progetto del team.
- Fare clic su Nuova definizione di compilazione.
- Nella sezione Trigger fare clic su Pianifica e selezionare i giorni in cui si desidera eseguire il code coverage.
- Nella sezione Elabora fare clic su Mostra dettagli nella sezione denominata Modello di processo di compilazione in alto e quindi selezionare il modello di compilazione archiviato nel controllo del codice sorgente.
- Compilare le altre sezioni richieste e salvare.
Aggiunta di un file di impostazioni test
Per la definizione di compilazione è necessario anche un file di impostazioni test. Si tratta di un file XML contenente le DLL per cui si desidera acquisire e pubblicare risultati. Ecco i passaggi per configurare questo file per il code coverage:
- Fare doppio clic sul file di impostazioni Local.test per aprire la finestra di dialogo Impostazioni test.
- Fare clic su Dati e diagnostica nell'elenco a sinistra.
- Fare clic su Code coverage e selezionare la casella di controllo.
- Fare clic sul pulsante Configura al di sopra dell'elenco.
- Selezionare la casella accanto alla DLL contenente i test (e il codice sottoposto a test).
- Deselezionare Instrumenta assembly sul posto, in quanto l'operazione verrà gestita dalla definizione di compilazione.
- Fare clic su OK, Applica e Chiudi.
Se si desidera compilare più di una soluzione o si dispone di più di un progetto di test, sarà necessaria una copia del file di impostazioni test che include i nomi di tutti gli assembly da monitorare per il code coverage.
A tale scopo, copiare il file di impostazioni test nella radice del ramo e attribuire uno nome descrittivo, ad esempio CC.testsettings. Modificare il file XML. Il file conterrà almeno un elemento CodeCoverageItem ricavato dai passaggi precedenti. È opportuno aggiungere una voce per ogni DLL che si desidera acquisire. I percorsi sono relativi alla posizione del file di progetto, non del file di impostazioni test. Verificare il file nel controllo del codice sorgente.
È infine necessario modificare la definizione di compilazione per utilizzare il file di impostazioni test:
- Nella finestra Esplora risorse espandere il nodo per il progetto del team, quindi espandere Compilazioni.
- Fare clic con il pulsante destro del mouse sulla definizione di compilazione creata in precedenza.
- Fare clic su Modifica definizione di compilazione.
- Nella sezione Processo espandere Test automatizzati, quindi 1. Assembly di test e fare clic su File TestSettings. Fare clic sul pulsante … e selezionare il file di impostazioni test creato in precedenza.
- Salvare le modifiche.
È possibile testare la definizione di compilazione facendo clic con il pulsante destro del mouse e scegliendo Accoda nuova compilazione per avviare una nuova compilazione.
Segnalazione di code coverage
Ho creato un report SQL Server Reporting Services personalizzato contenente il code coverage, come illustrato nella Figura 8 (per questioni di privacy ho sfocato i nomi dei progetti effettivi). Nel report viene utilizzata una query SQL per leggere i dati warehouse TFS e visualizzare i risultati combinati per il codice C++ e C#.
Figura 8 Report code coverage
Non analizzerò in dettaglio il funzionamento di questo report, ma vorrei citare un paio di aspetti. Il database contiene troppe informazioni relative al code coverage C++ per due motivi: il codice del metodo di test è incluso nei risultati e le librerie C++ standard (nei file di intestazione) sono inclusi nel risultati.
Ho aggiunto codice all'interno della query SQL per escludere questi dati aggiuntivi. Osservando il codice SQL all'interno del report, verrà visualizzato quanto segue:
and CodeElementName not like 'std::%'
and CodeElementName not like 'stdext::%'
and CodeElementName not like '`anonymous namespace'':%'
and CodeElementName not like '_bstr_t%'
and CodeElementName not like '_com_error%'
and CodeElementName not like '%Tests::%'
Queste righe escludono i risultati di code coverage per spazi dei nomi specifici (std, stdext e anonymous) e una coppia di classi predefinita di Visual C++ (_bstr_t e _com_error), nonché qualsiasi codice all'interno di uno spazio dei nomi che termina con Tests.
In quest'ultimo caso, escludendo gli spazi dei nomi che terminano con Tests, vengono esclusi eventuali metodi presenti nelle classi di test. Se si crea un nuovo progetto di test, poiché il nome di progetto termina con Tests, tutte le classi di test per impostazione predefinita si troveranno all'interno di uno spazio dei nomi che termina con Tests. È possibile aggiungere qui altre classi o spazi dei nomi che si desidera escludere.
In questo articolo ho fornito solo un accenno delle varie funzionalità disponibili. Per seguire il nostro avanzamento, consultare il blog all'indirizzo blogs.msdn.com/b/jsocha.
John Socha-Leialoha è uno sviluppatore del gruppo Management Platforms & Service Delivery presso Microsoft. Tra i suoi successi vengono annoverati la scrittura di Norton Commander (in C e assembler) e il testo "Peter Norton's Assembly Language Book" (Brady, 1987).
Un ringraziamento al seguente esperto tecnico per la revisione dell'articolo: Rong Lu