Pages

Showing posts with label Testing. Show all posts
Showing posts with label Testing. 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, 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.