Pages

Showing posts with label Extreme Programming. Show all posts
Showing posts with label Extreme Programming. Show all posts

Wednesday, January 18, 2012

Test Driven Development, C++ e Qt: Mock Objects

Dopo aver ripulito a dovere il codice siamo pronti per aggiungere nuove funzionalità alla nostra applicazione. Chiaramente seguendo le regole del tdd, ossia aggiungendo un nuovo test prima di scrivere nuovo codice.
Prima di iniziare, ho corretto un paio di minor issue sullo script bash che lancia i test, e sulla classe c++ che li raggruppa. In questo modo i test rispondono meglio alla linea di comando, ed è possibile isolare i singoli test case. Come al solito, trovate tutto sul repository git, l'url è in fondo alla pagina.

Una prima banale funzionalità che manca alla nostra applicazione è distinguere tra l'invocazione senza parametri, che come output stampa il messaggio di help, e l'invocazione con uno o più parametri, che invece fa altro.
Testare l'invocazione senza parametri è stato piuttosto facile, ma come testiamo l'invocazione con parametri, se non sappiamo ancora esattamente cosa farà?

Ci viene in aiuto la composizione fra oggetti: assumiamo infatti che la nostra classe DownloadManagerCore avrà uno (o più) collaboratori addetti al download, in tal caso la nostra classe dovrà semplicemente dare in pasto a questi collaboratori la lista degli url da scaricare.
Dobbiamo quindi identificare questi collaboratori, dare loro un nome (e quindi una responsabilità) e scrivere la loro interfaccia. Non ci interessa implementarli davvero, lo faremo in un secondo momento nei rispettivi test unitari, ci interessa però definire la loro interfaccia in modo da poterne creare istanze "finte" da usare nel test di DownloadManagerCore.

In una prima approssimazione possiamo pensare di creare un oggetto Queue, con un metodo add(QStringList) che viene invocato se l'oggetto DownloadManagerCore riceve una lista di argomenti non vuota.
Cominciamo quindi a creare la sua interfaccia: dato che si tratterà di un'interfaccia di dominio, creiamo il suo header nella sottodirectory domain del progetto downloadmanagercore.


class Queue {
public:
    virtual void add(const QStringList &urls)=0;
};



Ora possiamo scrivere una sua implementazione Fake (nel progetto UnitTests) su cui faremo delle asserzioni.
Gli oggetti Fake, detti anche Mock Objects, sono degli utili strumenti per verificare come gli oggetti interagiscono fra loro.
In realtà alcuni autori suggeriscono varie differenze tra mock objects, fake e stub, sopratutto legate alla loro programmabilità. Per semplicità al momento assumeremo equivalenti mock e fake, la rete è comunque piena di approfondimenti.
Vale la pena di specificare che in C++ è indispensabile che un oggetto abbia dei metodi virtual per poter essere correttamente mockato.
Un buon design fa comunicare fra loro le classi sopratutto tramite interfacce piuttosto che tramite oggetti   concreti, questo facilita molto la creazione di mock object adeguati.

Tornando alla classe FakeQueue, per poter fare delle asserzioni un field addWasCalled di tipo bool, e un field di tipo QStringList chiamato receivedUrls. Nel nuovo test verificheremo quindi che addWasCalled venga settato a true, e che receivedUrls sia uguale alla lista degli url ricevuti da DownloadManagerCore.
Questo tipo di oggetto permette di testare l'interazione tra la classe sotto test (DownloadManagerCore) ed i suoi collaboratori.

  • header *
class FakeQueue : public Queue
{
    Q_OBJECT
public:
    explicit FakeQueue(QObject *parent = 0);

    bool addWasCalled;
    QStringList receivedUrls;

    void add(const QStringList &urls);   
};


  • implementazione

FakeQueue::FakeQueue(QObject *parent) :
    Queue(parent), addWasCalled(false), receivedUrls(QStringList())
{
}

void FakeQueue::add(const QStringList &urls)
{
    addWasCalled=true;
    receivedUrls=urls;
}



Aggiungiamo il nuovo test case, che verifica che la chiamata add viene invocata istanziando DownloadManagerCore con almeno un argomento.



void DownloadManagerCoreTest::itShouldQueueUrlsWhenCalledWithArguments()
{
    QStringList urlList = QStringList() << "first url" << "second url";
    QString output;
    QTextStream outputStream(&output);
    FakeQueue queue;
    DownloadManagerCore *lib = new DownloadManagerCore(&outputStream, urlList, &queue, this);
    lib->start();
    QCOMPARE(queue.receivedUrls, urlList);
    QCOMPARE(output, QString() );
    QVERIFY2(queue.addWasCalled, "Add method should be called on queue");
}



In questo momento il test non compila neanche, questo perchè DownloadManagerCore non accetta ancora un puntatore a Queue in costruzione; come prima cosa quindi modifichiamo la firma del costruttore di DownloadManagerCore.

    
    DownloadManagerCore(QTextStream *output, const QStringList &arguments, Queue *queue, QObject *parent = 0);



Naturalmente dovremo sistemare anche gli altri test, che adesso avranno bisogno di Queue istanziando DownloadManagerCore.
Adesso vediamo finalmente il test fallire come ci aspettiamo: ci dice che si aspetta una QStringList di due stringe, e ne trova una vuota. Dobbiamo quindi far si che DownloadManagerCore accodi i parametri. Ma se invochiamo il metodo add su queue, il test fallisce perchè si aspetta che l'output (il messaggio di help) non venga scritto. Dobbiamo quindi aggiungere della logica di controllo dei parametri.


class DownloadManagerCorePrivate {
public:
    DownloadManagerCorePrivate(QTextStream *output, const QStringList &arguments, Queue *queue);
    QTextStream *output;
    QStringList arguments;
    Queue *queue;
    void printHelpMessage();
};


void DownloadManagerCore::start()
{
    Q_D(DownloadManagerCore);
    if(d->arguments.isEmpty()) {
        d->printHelpMessage();
        emit finished();
        return;
    }
    d->queue->add(d->arguments);
}



Adesso abbiamo bisogno di un'implementazione vera di Queue, in modo da poter anche compilare l'eseguibile principale. Questa classe deve:

  • ricevere la lista dei parametri
  • memorizzarla in una lista di url
  • effettuare validazioni
  • avviare il download
Sappiamo già che la classe Queue dovrà collaborare in qualche modo con QNetworkAccessManager, la classe di Qt addetta alle comunicazioni http.
Dovremo inoltre farla collaborare con un'altra entità, un FileWriter, che a fronte della ricezione di dati scrive il tutto su filesystem. Normalmente per testare questa classe è meglio far ricorso agli IntegrationTests, che però affronteremo più avanti. Se invece volessimo testare unitariamente una classe che collabora con alcune classi, come QNetworkAccessManager, di cui non possiamo creare implementazioni mock, come potremmo fare?
Purtroppo non c'è una risposta semplice, ed è proprio per questo che in questi casi si preferiscono di gran lunga i test di integrazione; tuttavia come esercizio proviamo ad elaborare una strategia.
Quella forse più efficace è di incapsulare le classi Qt da mockare in oggetti decorator, che implementeranno un'interfaccia che sarà la base del nostro mock object. In questo modo possiamo facilmente mockare i decorator, che saranno sufficientemente "stupidi" da non richiedere ulteriori test. Il decorator avrà anche il vantaggio di esporre solo i metodi che ci serviranno davvero, ripulendo l'interfaccia pubblica della classe decorata.
Per semplicità al momento ci occuperemo solo di testare la funzionalità di avvio e accodamento downlod.
Le classi che dovremo decorare saranno principalmente due:

  • QNetworkAccessManager, che diventerà NetworkAccess
  • QNetworkReply, che diventerà NetworkReply.

Chiaramente i test sono fatti apposta per evolversi man mano che le funzionalità vengono aggiunte, intanto però possiamo fare una lista dei metodi dell'interfaccia pubblica che avrà la classe NetworkAccess:
  • NetworkReply * get ( const QNetworkRequest & request)
  • [signal] void finished ( NetworkReply * reply )
Ecco invece l'interfaccia di NetworkReply:
  • [signal] void QIODevice::readyRead ()
  • QByteArray QIODevice::readAll ()
Quello che dovremo testare, in sintesi, è che l'implementazione HttpQueue dell'interfaccia Queue dovrà dire a NetworkReply di cominciare il download, fermandosi appena raggiunto un limite, accodando le richieste successive una volta finito uno dei download in corso.
Il codice risultante è un po' lunghetto da pubblicare qui, rimando quindi direttamente al branch github per questa puntata. In ogni caso, per quanto un po' tedioso, il test ha comunque fatto il suo dovere, permettendoci di sviluppare una funzionalità molto legata alla rete senza di fatto avviare alcuna connessione.


* normalmente è uno smell avere in una classe dei field pubblici. È però vero che questa è una classe non applicativa, che esiste solo nei test e che non ha vere responsabilità se non memorizzare il valore di quei field e fornirlo a chi li testa.


Tuesday, January 17, 2012

Test Driven Development, C++ e Qt: refactoring, ottimizzazioni, d-pointer

Nel precedente post abbiamo dato una messa a punto ai file del progetto. Potrebbe essere il caso di fare qualcosina anche per il codice sorgente...
In questa puntata vedremo alcuni passi di refactoring che ci aiuteranno ad implementare funzionalità future, e renderanno il codice più robusto. Vedremo ad esempio come applicare il meccanismo di Signal e Slot di Qt, particolarmente indicato per la programmazione "ad eventi", e in cosa consiste il pattern d-pointer che in alcuni ambienti (ad esempio nella programmazione per KDE) è divenuto uno standard praticamente obbligatorio

Sunday, January 8, 2012

Test Driven Development, C++ e Qt: ottimizzazioni layout progetti

Nella puntata precedente abbiamo creato un primo "vero" test di dominio perfettamente funzionante; il ciclo del TDD prevede a questo punto di effettuare un po' di refactoring, se necessario, prima di iniziare la nuova funzionalità.
In questo capitolo ci occuperemo non tanto di refactoring relativi al design (qualcosa si può fare, ma avendo scritto pochissimo codice è davvero poca roba), quanto piuttosto di trasformare quello che adesso è codice "sperimentale" in qualcosa di più professionale, usando layout di progetto, ottimizzazioni e convenzioni che sono da favorire in progetti pronti per la produzione.
Ma che problemi ha il nostro progettino?
In realtà un bel po', cominciamo dalle cose più evidenti: la prima è che i progetti sono collegati tra loro piuttosto malamente.
Sappiamo che il progetto DownloadManager deve usare DownloadManagerCore, ma al momento non c'è nessun riferimento ad esso. Sappiamo anche che il progetto UnitTests deve fare, e in realtà fa già, la stessa cosa. Ma per farlo abbiamo dovuto aggiungere la riga:

    LIBS += -L../DownloadManagerCore-build-desktop/ -lDownloadManagerCore

e di certo non vorremo tenerla così: quando infatti dovremo compilare in modalità release sarebbe necessario modificare manualmente il file di progetto (tanto nel progetto UnitTests quanto in DownloadManager), e non è sicuramente il massimo!
Ma anche nel codice sorgente c'è qualcosina che non va: in downloadmanagercoretest.cpp vediamo la riga

    #include "../DownloadManagerCore/downloadmanagercore.h"

che dovremmo ripetere per ogni futuro test case. Non va bene!

Per fortuna qmake ci viene in aiuto con alcune funzioni interessanti.
Aggiungendo al file di progetto la riga

    INCLUDEPATH += ../DownloadManagerCore

rendiamo disponibli tutti gli header di DownloadManagerCore al progetto UnitTests, trasformando quindi la riga precedente in

    #include "downloadmanagercore.h"

Applichiamo questa modifica anche al progetto DownloadManager.

Per distinguere invece tra debug e release, usando anche un path migliore per la direttiva LIBS dobbiamo invece usare una funzionalità un po' più avanzata di qmake.
L'idea alla base è di compilare DownloadManagerCore, installarlo in una directory comune (con suffisso -debug o -release a seconda di come l'abbiamo compilata) e far puntare gli altri progetti a quella directory.
Per far questo dobbiamo aggiungere questa sezione a tutti i file di progetto (quindi DownloadManagerCore, DownloadManager e UnitTests):


win32 {
    build_pass:CONFIG(debug, debug|release)   { COMPILE_MODE='debug'   }
    else:build_pass                           { COMPILE_MODE='release' }
} else {
    CONFIG(debug, debug|release)   { COMPILE_MODE='debug' }
    else                           { COMPILE_MODE='release' }
}


Brevemente, viene assegnata una variabile COMPILE_MODE con un valore dipendente da come si compila il progetto corrente.
win32 viene gestito a parte perchè ho notato che COMPILE_MODE rimane vuota con la sintassi specificata nel ramo else.
Probabilmente dipende dal tipo di compilatore in uso, la sezione nel ramo win32 è stata testata su visual c++ 2010, probabilmente usando invece mingw non è ncessaria.
In ogni caso, provate a vedere se COMPILE_MODE viene popolata o meno usando l'istruzione


message("Using compile mode: $$COMPILE_MODE")



Adesso che abbiamo definito la variabile, proviamo ad usarla.
Nel progetto DownloadManagerCore aggiungiamo il path per l'installazione.



target.path='../static-libraries.bin/$$COMPILE_MODE'
INSTALLS += target


Questo farà in modo che eseguendo make install la libreria verrà copiata nella directory static-libraries.bin/debug (o release).
È consigliabile anche modificare i profili del progetto in qt creator: in "projects->build settings", tra i build steps, aggiungiamo al passo make l'argomento install. Questo per tutti i profili di DownloadManagerCore.
In questo modo, ad ogni build, la libreria statica verrà installata automaticamente.
Ora nei progetti DownloadManager e UnitTests andiamo ad usare questa nuova funzionalità. Molto semplicemente, la riga "LIBS+= ....." vista in precedenza, diventerà:


LIBS += -L../static-libraries.bin/$$COMPILE_MODE -lDownloadManagerCore

aggiungendola a DownloadManager dove non esisteva ancora.
Il modo migliore per verificare il risultato di quello che abbiamo fatto è cancellare tutte le directory di build, ricompilare da capo il progetto UnitTests (e naturalmente la sua dipendenza, DownloadManagerCore) e verificare che i test passino ancora.

Un' ultima ottimizzazione: risulta spesso utile in molti progetti avere una directory dove inserire script per automatizzare i task più frequenti, che spesso richiederebbero una serie di operazioni manuali non proprio comode. Creiamo quindi una directory "scripts". Che script possiamo scrivere per inaugurarla?
Beh, un primo task noioso e facilmente automatizzabile è la compilazione ed esecuzione automatica dei test: vogliamo uno script che sia in grado di compilare anzitutto il progetto DownloadManagerCore, installarlo nella directory static-libraries.bin, quindi compilare ed eseguire automaticamente i tests.

#!/bin/bash
set -e

PROJECTS="UnitTests"
COLORIZE_CMD="ccze -A"

MAIN_PROJECT="DownloadManagerCore"

BASE_DIRECTORY="$PWD/$( dirname $0)/.."
COMPILE_BUILD_PREFIX="$BASE_DIRECTORY/.build-"


if test "x$TERM" != "xxterm" || test "x$( which $( echo $COLORIZE_CMD | cut -d " " -f 1 ) )" == "x"; then #TODO add other terminals supported by ccze here
        COLORIZE_CMD="cat"
fi

if test "x$1" == "x--help"; then
        echo "Usage: $0 [Test-Project-Name [test-method-name]]"
        echo "It runs every project configured in the PROJECTS variable, or can select from parameters"
        echo "The variable MODE can be used to choose between debug and release compilation"
        echo "Currently supported test projects: $PROJECTS"
        exit 0
fi

if test "x$1" != "x"; then
        PROJECTS="$1"
        [ -f $PROJECTS ] && PROJECTS="$( basename "$( dirname "$PROJECTS")" )"
        echo "**** Project: $PROJECTS"
        shift
fi

BUILD_MODE="${MODE-debug}"

function buildProject() {
        project="$1"
        shift
        make_args="$1"
        shift
        #rm -rf "$project.build"
        mkdir -p "${COMPILE_BUILD_PREFIX}$project"
        cd "${COMPILE_BUILD_PREFIX}$project"
        qmake "CONFIG+=$BUILD_MODE silent" "DEFINES += QT_NO_DEBUG_OUTPUT" "$@" "../$project"
        make $make_args > /dev/null
        cd ..
}


function runTests() {
        suitename="$1"
        shift
        echo -e "Running suite $suitename\n"
        cd "${COMPILE_BUILD_PREFIX}$suitename"
        binary="./$suitename" 
        $binary $@ | while read line; do echo "$line" | $COLORIZE_CMD ; done
        ERRORS=${PIPESTATUS[0]}
        cd ..
        if test "x$ERRORS" == "x0"; then
                echo -e "\nSuite $suitename completed without errors\n"
        else
                echo -e "Suite $suitename finished with errors!\n"
                exit $ERRORS
        fi
}


buildProject $MAIN_PROJECT "all install"
for project in $PROJECTS; do
        buildProject $project "clean all" "LIBS+=-L../${COMPILE_BUILD_PREFIX}$MAIN_PROJECT" 
        runTests $project $@
done


Come al solito, i sorgenti per questa puntata sono disponibili sul mio account github.



Nella prossima puntata ci occuperemo invece di refactoring e ottimizzazioni del codice vero e proprio.

Friday, January 6, 2012

Test Driven Development, C++ e Qt: primi passi (e test)

Dopo aver creato la struttura del progetto nella prima parte, cominciamo adesso ad implementare l'applicazione.
Guardiamo l'esempio di Qt da cui stiamo prendendo spunto: il main prende in ingresso la lista di downloads (usando QCoreApplication, che li converte automaticamente in una comoda QStringList), e, se la lista è vuota stampa le informazioni di utilizzo, uscendo.
Potremmo andare avanti ad analizzare le successive funzionalità, certo l'applicazione non si limita a far questo, ma il tdd ci impone di creare un primo test (che inizialmente fallisce), ed implementare la funzionalità facendo passare il test prima di procedere troppo oltre. In una applicazione così semplice, è meglio semplificare anche l'approccio evitando di pensar troppo al futuro.
L'esempio mette il messaggio di errore nel main. Così facendo però non potremmo testarlo. D'altro canto è proprio il main che costruisce la lista di argomenti. Un buon compromesso quindi è di creare un metodo download(const QStringList &arguments)* nella classe DownloadManagerCore che come risultato stampi le informazioni di utilizzo, usando gli argomenti creati da QCoreApplication.
Può inoltre aiutare molto far estendere QObject alla classe DownloadManagerCore; in questo modo si ottimizza la memoria (c++ non ha garbage collector, però ogni classe che estende QObject verrà automaticamente distrutta quando viene distrutta la classe parent) e si migliora l'iterazione con le classi che  verranno usate, le quali saranno infatti a loro volta derivate da QObject.
Nel main.cpp invocheremo QCoreApplication per estrarre gli argomenti, e invocheremo DownloadManagerCore::download(arguments).

Proviamo quindi a scrivere il primo test. In particolare asseriremo che il metodo download stamperà su standard output (o meglio, su un output stream testabile), se la lista degli argomenti è vuota, la seguente stringa:
Usage: downloadmanager url1 [url2... urlN]
Downloads the URLs passed in the command-line to the local directory.If the target file already exists, a .0, .1, .2, etc. is appended to differentiate.

Creiamo quindi il test case itShouldPrintHelpMessageWithNoArguments.

void DownloadManagerCoreTest::itShouldPrintHelpMessageWithNoArguments()
{
    QString output;
    QTextStream outputStream(&output);
    DownloadManagerCore *lib = new DownloadManagerCore(&outputStream, this);
    lib->start(QStringList());
    QString expected("Usage: downloadmanager url1 [url2... urlN]\n");
    expected.append("Downloads the URLs passed in the command-line to the local directory.");
    expected.append(" If the target file already exists, a .0, .1, .2, etc. is appended to differentiate.\n");
    QCOMPARE(output, expected);
}

Al di là della funzionalità, di per se banale, notiamo un po' di cose importanti su come il test stia definendo in maniera molto precisa il design della classe:

  • Abbiamo bisogno di testare l'output. È quindi da evitare che la classe stampi direttamente l'output su schermo. Definiamo invece un costruttore che prende in ingresso un puntatore a QTextStream e facciamo delle asserzioni su quello. Lo stream di test, in particolare, sarà uno stream che scrive su una stringa anzichè su stdout.
  • Abbiamo anche bisogno di una entry point che riceva in ingresso gli argomenti. Definiamo quindi il metodo "start" nella classe DownloadManagerCore, il cui header diventerà quindi questo:
#ifndef DOWNLOADMANAGERCORE_H
#define DOWNLOADMANAGERCORE_H
#include <QObject>

class QTextStream;
class DownloadManagerCore : public QObject {
    Q_OBJECT
public:
    DownloadManagerCore(QTextStream *output);
    void start(const QStringList &arguments);
private:
    QTextStream *output;
};

#endif // DOWNLOADMANAGERCORE_H

Ho anche fatto estendere QObject a QDownloadManagerCore, in questo modo si potrà usare il meccanismo di auto-delete di Qt: se le classi del nostro progetto estendono QObject, e si assegna loro una classe "parent", le classi saranno automaticamente cancellate quando sarà cancellata la loro classe parent, senza nessun bisogno di cancellarle manualmente. In questo modo è possibile ridurre al minimo i rischi di memory leak, potendo assegnare alle classi uno scope ben preciso. Maggiori dettagli qui.

Naturalmente il nostro test non può ancora passare: abbiamo definito l'interfaccia della nostra classe, ma non l'implementazione.
Per avere un primo test verde basta usare lo stream per stampare in output l'help message, senza preoccuparci degli argomenti, dato che per il momento nessun test ne verifica l'utilizzo.

#include "downloadmanagercore.h"
#include <QTextStream>

DownloadManagerCore::DownloadManagerCore(QTextStream *output, QObject *parent)
    : QObject(parent), output(output)
{
}

void DownloadManagerCore::start(const QStringList &arguments)
{
    *output << "Usage: downloadmanager url1 [url2... urlN]" << endl;
    *output << "Downloads the URLs passed in the command-line to the local directory.";
    *output << " If the target file already exists, a .0, .1, .2, etc. is appended to differentiate." << endl;
}

E finalmente, barra verde!

********* Start testing of DownloadManagerCoreTest ********* 
Config: Using QTest library 4.7.4, Qt 4.7.4 
PASS   : DownloadManagerCoreTest::initTestCase() 
PASS   : DownloadManagerCoreTest::itShouldLinkToStaticLibrary() 
PASS   : DownloadManagerCoreTest::itShouldPrintHelpMessageWithNoArguments() 
PASS   : DownloadManagerCoreTest::cleanupTestCase() 
Totals: 4 passed, 0 failed, 0 skipped 
********* Finished testing of DownloadManagerCoreTest ********* 

La prossima volta vedremo di ottimizzare il nostro codice prima di aggiungere nuove funzionalità: refactoring, ma anche migliorie del layout del progetto e ottimizzazioni varie.
Qui il codice sorgente per questa puntata.




* la sintassi "const QString &arg" o "const QStringList &arg" è del tutto equivalente a specificare "QString arg". In genere però è molto meglio specificare il "const" e la "&", per ottimizzare le chiamate. In questo modo infatti i parametri saranno immutabili e passati esplicitamente per riferimento, anzichè ricopiate nel metodo, (occupando quindi più memoria e tempo cpu). Sono piccoli accorgimenti da memorizzare ed usare con costanza, che possono però migliorare di molto le prestazioni del proprio codice.

Sunday, December 11, 2011

Test Driven Development, C++ e Qt: setup e layout progetto

I metodi agili cominciano a prendere piede anche in italia, e con essi quello che è probabilmente il loro più conosciuto cavallo di battaglia: il TDD, ossia Test Driven Development. Uno dei linguaggi in cui il TDD viene più spesso è applicato è java: non solo perchè è uno dei più diffusi, ma probabilmente perchè offre allo sviluppatore degli strumenti veramente potenti: da JUnit, scritto proprio da Kent Beck, ad Eclipse, che oltre ad integrare un perfetto supporto a JUnit offre anche dei potentissimi strumenti per il refactoring, che è parte integrante di questa tecnica.
Ma per quanto riguarda altri linguaggi, come il vetusto ma sempre attuale C++? Come si può conciliare la "dinamicità" di un processo come il TDD alla macchinosità di un linguaggio che prevede costante sincronizzazione header-implementazione, build system non sempre amichevoli e pochissimi tool di refactoring?
In questa guida cercherò di offrire uno dei possibili approcci. Dovendo scegliere un IDE e un toolkit ho optato per Qt di Nokia (originariamente di Trolltech) e il suo IDE Qt Creator.
Ovviamente ognuno avrà le sue preferenze, il mondo del C++ è fin troppo ricco. Per quanto mi riguarda, da utente e sviluppatore KDE, conosco Qt da molti anni, lo trovo piuttosto semplice e intuitivo, e altrettanto può dirsi di Qt Creator, sicuramente non un IDE potente e pieno di strumenti come Eclipse, ma che ha dalla sua una notevole leggerezza, nonchè l'essere studiato proprio per Qt.
Qt offre un ambiente molto omogeneo, includendo anche un modulo per i test unitari. Sicuramente ci sono framework di testi anche migliori, ma per continuare sulla via della semplicità ho preferito usare QTest. Ha anche il non indifferente vantaggio di essere orientato al cross platform, il che vuol dire che un'applicazione o libreria creata con Qt girerà sui principali sistemi operativi (Windows, Mac, Linux) senza modificare (o quasi) il codice, ma solo ricompilando per i sistemi desiderati.

Disclaimer: non oso definirmi nè un esperto di C++ nè tantomeno di Qt. Ho imparato entrambi amatorialmente, e sono un po' arrugginito dai molti anni lavorativi in cui ho utilizzato quasi esclusivamente Java.


Come progetto di esempio prenderò spunto dal Download Manager presente nella documentazione di Qt. Si tratta di un eseguibile da riga di comando, senza GUI, anche molto semplice da realizzare.

Andiamo a creare il layout del nostro progetto.
Da QtCreator sceglieremo l'opzione "New Project", scegliendo tra i vari template disponibili "Qt C++ Project << Qt Console Application". Lo chiameremo "DownloadManager", e lo creeremo nella directory "$HOME/qt".
Il progetto verrà a trovarsi quindi in "DownloadManager", avrà un main pressochè vuoto.

Adesso cominciamo a creare le prime classi? Sbagliato. In realtà il progetto "DownloadManager" è praticamente già finito. Tutto il nostro codice andrà invece in una libreria statica: questo perchè dovrà essere accessibile anche dai test. Una libreria statica è molto diversa da una dinamica: essa non verrà infatti "esportata" come una dipendenza, ma viene inglobata dall'eseguibile, avendo però il vantaggio di poter condividere le proprie classi anche con i test che creeremo.
Quindi, nuovo progetto, con tipo "Other Project << C++ Library". Scegliamo nel wizard "Statically Linked Library", assegnamo come nome "DownloadManagerCore" e lo creiamo sempre nella posizione "$HOME/qt". Nella pagina successiva scegliamo di aggiungere i moduli "QtCore" e "QtNetwork". Il wizard creerà anche una classe principale (che potremo usare come un main) DownloadManagerCore.

Adesso è ora di creare il progetto (o come vedremo, i progetti) per i test, cominciando dagli unit: sempre dal menù nuovo progetto, scegliamo nuovamente "Other Project << Qt Console Application", con nome "UnitTests" e solita posizione, "$HOME/qt". Nel file UnitTests.pro creato dal wizard cambiamo la riga

   QT       += core
in
   QT       += core testlib



Col tasto destro sul progetto selezioniamo "Add New...", quindi scegliamo "C++ Class".
Diamo nome alla classe: "DownloadManagerCoreTest", base class "QObject".
Modifichiamo lo header creando un primo test come private slot:

#ifndef DOWNLOADMANAGERCORETEST_H
#define DOWNLOADMANAGERCORETEST_H

#include <QObject>

class DownloadManagerCoreTest : public QObject
{
    Q_OBJECT
public:
    explicit DownloadManagerCoreTest(QObject *parent = 0);

private slots:
    void itShouldLinkToStaticLibrary();

};

#endif // DOWNLOADMANAGERCORETEST_H


e anche la sua implementazione:

#include <QTest>

#include "downloadmanagercoretest.h"
#include "../DownloadManagerCore/downloadmanagercore.h"

DownloadManagerCoreTest::DownloadManagerCoreTest(QObject *parent) :
    QObject(parent)
{
}

void DownloadManagerCoreTest::itShouldLinkToStaticLibrary() {
    DownloadManagerCore *lib = new DownloadManagerCore();
    QVERIFY2(lib != NULL, "Library should not be null");
}

QVERIFY è una semplice asserzione che verifica il valore booleano passato come parametro.
QVERIFY2 aggiunge la possibilità di specificare un messaggio.
Questi sono un po' i "mattoncini base" per le asserzioni; ne esistono altre, ed è anche possibile implementarne di personalizzate.
Modificare anche il main.cpp come segue:

#include <QtCore/QString>
#include <QtTest/QTest>
#include <QList>

#include "downloadmanagercoretest.h"

class UnitTests : public QObject
{
    Q_OBJECT
private:
    int argc;
    char **argv;

public:
    UnitTests(int argc, char *argv[]);
    int runAll();
private:
    QList<QObject*> tests;
};

UnitTests::UnitTests(int argc, char *argv[])
    : argc(argc), argv(argv)
{
    tests << new DownloadManagerCoreTest(this);
}

int UnitTests::runAll() {
    int testResults=0;
    foreach(QObject *currentTest, tests) {
        testResults += QTest::qExec(currentTest, argc, argv);
    }
    return testResults;
}


int main(int argc, char *argv[])
{
    UnitTests testSuite(argc, argv);
    return testSuite.runAll();
}

#include "main.moc"


Aggiungere nuovi test alla suite è semplice, basta creare dei metodi (void) come "private slots" nelle classi di test.
Se vorremo aggiungere nuove classi di test alla test suite, basterà invece creare nuove classi del tutto simili a DownloadManagerCoreTest: molto semplicemente devono estendere QObject e tutti i metodi dichiarati "private slot" verranno automaticamente usati come test case.
Si deve poi includere lo header corretto nel main.cpp ed aggiungere una riga uguale all'esistente nel costruttore di UnitTests per aggiungere la classe di test alla suite.
Infine, le altre suite di test, come gli integration test e gli acceptance test, possono essere creati esattamente nello stesso modo del progetto UnitTests.

Ma che succede se proviamo ad eseguire il nostro primo test unitario?
Non compila nulla.. o per meglio dire, il link fallisce: questo perchè il nostro progetto di test non "vede" ancora il progetto contenente la classe "DownloadManagerCore". Come Fare?
Beh, basta aprire nuovamente il file "UnitTests.pro" ed aggiungere alla fine del file la riga


   LIBS += -L../DownloadManagerCore-build-desktop/ -lDownloadManagerCore



Visto che anche l'applicazione "DownloadManager" richiederà la libreria può essere utile aggiungere questa riga anche nel suo file .pro.
Può anche essere utile andare nel tab "Projects" di Qt Creator, e nei progetti "DownloadManager" e "UnitTests" selezionare come dipendenza (tab Dependencies) il progetto "DownloadManagerCore".

Adesso basta compilare il tutto per avere la suite di test funzionante:
********* Start testing of DownloadManagerCoreTest *********
Config: Using QTest library 4.7.4, Qt 4.7.4
PASS   : DownloadManagerCoreTest::initTestCase() 
PASS   : DownloadManagerCoreTest::itShouldLinkToStaticLibrary()
PASS   : DownloadManagerCoreTest::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of DownloadManagerCoreTest *********
Alla prossima puntata!

p.s.: i sorgenti per questa versione del progetto potete trovarmi sul mio account github.