Pages

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.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.