Pages

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.