Qt and CppUnit usage example

by Philippe Fremy




In this example, I will build and test a search dialog, using Qt. I'll follow very closely the Java example of the book "Extrem Programming Installed", chapter 31 ("A Java perspective"), page 241.

The complete archive of this example is available here

So we imagine that we have an application that somewhere must be able to perform search queries that will return lists of documents.

The application

First, I'll build my application. We have a main.cpp to create the QApplication, mainWindow.h and mainWindow.cpp for a main window that contains one menubar, one statusbar and a big label as central widget.  All these files are put in a simple project file example2.pro which will generate the Makefile, thank to tmake.

example2.pro
TEMPLATE    = app
CONFIG        = qt warn_on debug
unix:LIBS   = -L../qcppunit -lqcppunit
INCLUDEPATH = ../qcppunit ../qcppunit/testlib
HEADERS        = \
        mainWindow.h


SOURCES        = \
        main.cpp        \
        mainWindow.cpp

TARGET = app2


main.cpp
#include <qapplication.h>

#include "mainWindow.h"

int main( int ac, char ** av )
{
    QApplication a( ac, av );

    MainWindow * mw = new MainWindow( "MainWindow" );
    a.setMainWidget( mw );
    mw->show();
    a.exec();

    return 0;
}


mainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <qmainwindow.h>

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow( const char * name );

public slots:
    void search();

};

#endif // MAINWINDOW_H



mainWindow.cpp
#include <qlabel.h>
#include <qlayout.h>
#include <qpopupmenu.h>
#include <qmenubar.h>
#include <qapplication.h>

#include "mainWindow.h"

MainWindow::MainWindow( const char * name )
: QMainWindow( 0L, name )
{
    setCaption("An application");

    // Main Widget
    QLabel * mainWidget = new QLabel("A main widget!", this );
    setCentralWidget( mainWidget );
    mainWidget->setAlignment( AlignVCenter | AlignHCenter );

    // Menu bar
    QPopupMenu* file = new QPopupMenu( this );
    file->insertItem("E&xit", qApp, SLOT( quit() ) );
    menuBar()->insertItem( "&File", file );

    QPopupMenu* edit = new QPopupMenu( this );
    edit->insertItem("Search", this, SLOT( search() ) );
    menuBar()->insertItem( "&Edit", edit );

    // Status bar
    statusBar();   
}

void MainWindow::search()
{
    qDebug("search!");
}


Creating the test suite

We now create a test suite that will be responsible for testing the Search dialog. Our test suite will be a class TestSearchDialog and will reside in the files testSearchDialog.h and testSearchDialog.cpp .

A test suite is contained in an object TestCase. We usually add some Tests to the suite and then return it. For the moment we have no test so we return an emtpy TestSuite object.
testSearchDialog.h
#ifndef TESTSEARCHDIALOG_H
#define TESTSEARCHDIALOG_H

#include "TestCase.h"
#include "TestSuite.h"
#include "TestCaller.h"

class TestSearchDialog : public TestCase
{
public:
    TestSearchDialog(const char * name )
        : TestCase(name)
    {}

    void setUp () {}
    void tearDown() {}

    static Test    *suite ()
    {
        TestSuite *testSuite = new TestSuite ("Search Dialog");
        return testSuite;
    }

};

#endif // TESTSEARCHDIALOG_H



testSearchDialog.cpp:
#include "testSearchDialog.h"


And we add the two files to the project file :

example2.pro
TEMPLATE    = app
CONFIG        = qt warn_on debug
unix:LIBS   = -L../qcppunit -lqcppunit
INCLUDEPATH = ../qcppunit ../qcppunit/testlib
HEADERS        = \
        mainWindow.h    \
        testSearchDialog.h  



SOURCES        = \
        main.cpp        \
        mainWindow.cpp  \
        testSearchDialog.cpp 


TARGET = app2


We fix various compilation problems.  It runs. Cool!

Creating the test runner dialog.

The object responsible for running the tests is GuiTestRunner. It inherits a QDialog. You only require to instantiate a Qt application to run a dialog. So the test can perfectely be runned outside any application. Here, we will choose to integrate the test running dialog inside our application, because this is more fun.

We will create a slot that will invoke the GuiTestRunner dialog. A menu entry gives access to the test dialog. The slot will create a GuiTestRunner, to which we will add our test suite.

mainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <qmainwindow.h>

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow( const char * name );

public slots:
    void search();
    void runTestDialog();

};

#endif // MAINWINDOW_H



new mainWindow.cpp
#include <qlabel.h>
#include <qlayout.h>
#include <qpopupmenu.h>
#include <qmenubar.h>
#include <qapplication.h>

#include "mainWindow.h"
#include "guiRunner.h"
#include "testSearchDialog.h"

MainWindow::MainWindow( const char * name )
: QMainWindow( 0L, name )
{
    setCaption("An application");

    // Main Widget
    QLabel * mainWidget = new QLabel("A main widget!", this );
    setCentralWidget( mainWidget );
    mainWidget->setAlignment( AlignVCenter | AlignHCenter );

    // Menu bar
    QPopupMenu* file = new QPopupMenu( this );
    file->insertItem("E&xit", qApp, SLOT( quit() ) );
    menuBar()->insertItem( "&File", file );

    QPopupMenu* edit = new QPopupMenu( this );
    edit->insertItem("Search", this, SLOT( search() ) );
    menuBar()->insertItem( "&Edit", edit );

    QPopupMenu* test = new QPopupMenu( this );
    test->insertItem("test", this, SLOT( runTestDialog() ) );
    menuBar()->insertItem( "&Test", test );

    // Status bar
    statusBar();   
}

void MainWindow::search()
{
    qDebug("search!");
}

void MainWindow::runTestDialog()
{
    GuiTestRunner * runner = new GuiTestRunner;

    // add your test suites here
    Test * suite;
   
    suite = TestSearchDialog::suite();
    runner->addTest( suite->toString(), suite );

    runner->show();
}


Add all this, fix the compilation problems. Run. There is a new menu Test with an entry "test". When we click on it, a dialog appears.

The test dialog contains two buttons : "Run all" to run all tests, "Run selected" to run only the selected test.  A progress bar at 0% is also there. The progress bar reports the test success. No test, so 0% completed.

Below, there is a box presenting our Test suite which reads "suite Search Dialog" and "0/0" in green. This will be a lot more meaningful when we have some tests.

Our first test.

The goal is to test a Search Dialog. For the moment, we have no search dialog, so we will just make a test that creates a Search dialog. That should be enough.

In our TestSearchDialog class, we add a member function, testCreate() that will perform the test. In our implementation, we create a SearchDialog :

testSearchDialog.cpp
void TestSearchDialog::testCreate()
{
    SearchDialog * sd = new SearchDialog();
    sd->exec();
    sd->setCaption("Search Dialog");
    delete sd;
}


Hint: this will not compile.

To make our first test succeed (i.e. compile), we need to create a searchDialog class. Ok, we create two files searchDialog.h and searchDialog.cpp that contains a SearchDialog that inherits from QDialog.

searchDialog.h:
#ifndef SEARCHDIALOG_H
#define SEARCHDIALOG_H

#include <qdialog.h>

class SearchDialog : public QDialog
{
public:
    SearchDialog();
   
};

#endif // SEARCHDIALOG_H



searchDialog.cpp
#include "searchDialog.h"

SearchDialog::SearchDialog()
: QDialog(0, "Search Dialog", true )
{
}


Now, it compiles. We must add our new created test to our test suite :

testSearchDialog.h
#ifndef TESTSEARCHDIALOG_H
#define TESTSEARCHDIALOG_H

#include "TestCase.h"
#include "TestSuite.h"
#include "TestCaller.h"

class TestSearchDialog : public TestCase
{
public:
    TestSearchDialog(const char * name )
        : TestCase(name)
    {}

    void setUp () {}
    void tearDown() {}

    static Test    *suite ()
    {
        TestSuite *testSuite = new TestSuite ("Search Dialog");

        testSuite->addTest( new TestCaller<TestSearchDialog> ("testCreate", &TestSearchDialog::testCreate ));

        return testSuite;
    }

protected:
    // My tests
    void testCreate();
};

#endif // TESTSEARCHDIALOG_H


Now, when we launch the test dialog, the progress bar goes from 0% to 100% and is green. This means that all tests were successfully run. Our test suite item's color is green, which means all its tests were successful. It reads "1/1" : 1 test successful on a total of 1 test. Cool, everything works.

Seeing the dialog

We now know that we can create a test dialog. The next step is to put things in it, that will make the dialog useful. To be useful, our search dialog must contain:
Our next step will be to check the presence of these widgets. So we create a test testWidgetPresent() in our test suite and implement it. Since we will need a created SearchDialog in each tests, we will create it in the setUp() function of the suite, and delete it in the tearDown() function. That way, it will be created before each test and deleted after each test. We also add a waiting time of one second after each tests, so that one can actually look at the tests while they are executed. It is not necessary but more pleasant.

First problem, how do I make the member of SearchDialog accessible to the test class ? First solution, declare the TestSearchDialog class as friend. Second solution, make TestSearchDialog inherit from SearchDialog. I prefer the first solution because I prefer to access my tested class from outside. But this is only a matter of taste.

We add what is necessary to testSearchDialog.h:
testSearchDialog.h extract
class TestSearchDialog : public TestCase
{
public:
    TestSearchDialog(const char * name )
        : TestCase(name)
    {}

    void setUp ();
    void tearDown();

    static Test    *suite ()
    {
        TestSuite *testSuite = new TestSuite ("Search Dialog");

        testSuite->addTest( new TestCaller<TestSearchDialog> ("testCreate", &TestSearchDialog::testCreate ));
        testSuite->addTest( new TestCaller<TestSearchDialog> ("testWidgetPresent", &TestSearchDialog::testWidgetPresent ));

        return testSuite;
    }

protected:
    // My tests
    void testCreate();
    void testWidgetPresent();

    SearchDialog * _sd;

};




Here is our new test with the two new methods setUp() and tearDown(). The function check() is provided by the class TestCase and report a test failure (with the information for where it failed) if its argument is not true. The other useful testing function is checkEquals( a, b ) that will report an error if a is not equal to b, a and b being either integers, doubles or QString.


void TestSearchDialog::setUp()
{
    _sd = new SearchDialog();
    _sd->setCaption( "Search dialog");
    _sd->show();
}

void TestSearchDialog::tearDown()
{
    QTime t;
    t.start();
    while( t.elapsed() < 1000 ) {
        qApp->processEvents();
    }

    delete _sd;
    _sd = 0;
}

void TestSearchDialog::testCreate()
{
    SearchDialog * sd = new SearchDialog();
    sd->setCaption( "Search dialog");
    sd->show();
    delete sd;
}

void TestSearchDialog::testWidgetPresent()
{
    check( _sd->_queryLabel != 0l );
    check( _sd->_queryText != 0l );
    check( _sd->_buttonSearch != 0l );
    check( _sd->_resultWidget != 0l );
}


To make the test compile, we must add some members to SearchDialog:
searchDialog.h
#ifndef SEARCHDIALOG_H
#define SEARCHDIALOG_H

#include <qdialog.h>
#include <qlineedit.h>
#include <qlabel.h>
#include <qpushbutton.h>
#include <qlistview.h>


class TestSearchDialog;

class SearchDialog : public QDialog
{
public:
    SearchDialog();

protected:
    QLabel * _queryLabel;
    QLineEdit * _queryText;
    QPushButton * _buttonSearch;
    QListView * _resultWidget;


friend TestSearchDialog;

};

#endif // SEARCHDIALOG_H

It compiles, it runs. We launch the test dialog. This times, the progress bar is green until it gets to 50%. At this time, a window appears on the screen and disappears. The the progress bar turns red, a new window appears and disappears.

We read the text : "1 of 2 tests successful. 1 test witl failures". Mmh, our suite is also displayed in red, with "1 / 2". Things are clear, a test did not pass. An item under our suite has been added. This is the failed test. It reads:
testWidgetPresent    testSearchDialog.cpp    40    "_sd->_queryLabel != 0L"
the test testWidgetPresent failed, in the file testSearchDialog.cpp, line 40 and the failure cause is "_sd->queryLabel != 0L" is not true.

In our case, we have just added new members to SearchDialog but did not create the assiociated widget. gcc gently set those new pointers to NULL, making the test fail. We now create the widgets. We could have created them right from the start. But it is always intersting to see a test fail. It is reassuring that it will actually detect a failure.

We improve our search dialog constructor :
searchDialog.cpp extract
SearchDialog::SearchDialog()
: QDialog(0, "Search Dialog", false)
{
    QVBoxLayout * vly = new QVBoxLayout( this );
    vly->setAutoAdd( true );

    QHBox * hbox = new QHBox( this );
    _queryLabel = new QLabel( "Query : ", hbox );
    _queryText = new QLineEdit( hbox );
    _buttonSearch = new QPushButton( "Search", hbox );

    _resultWidget = new QListView( this );
}

Compile, run. The test passes and you see the dialog popping up with the new items we have just added.

We improve the test to check that the button and the label contain some text, and that the text field is empty.

testSearchDialog.cpp extract
void TestSearchDialog::testWidgetPresent()
{
    check( _sd->_queryLabel != 0l );
    check( _sd->_queryLabel->text().isEmpty() == false );
    check( _sd->_queryText != 0l );
    check( _sd->_queryText->text().isEmpty() == true );
    check( _sd->_buttonSearch != 0l );
    check( _sd->_buttonSearch->text().isEmpty() == false );
    check( _sd->_resultWidget != 0l );
    check( _sd->_resultWidget->childCount() == 0 );
}


We could have tested the exact text of queryLabel and buttonSearch but this would make the test fail if we decide to change the text. Our tests are here for testing behaviour, not look and feel. A button and a label must not be empty, this is important. But one should be able to change their content (for example if we translate them) without breaking the tests.

You have also noticed that our test doesn't depend upon the position of the widget. Again, this is pure look and feel. One could rearrange the dialog, add more buttons and fancy stuff, if these four items are there, the user can perform a search so our test is ok.

Need some backend

Now that we have our dialog, we must perform some search. We will need a search backend. We suppose that another team is responsible for the search backend and they are not finished yet so we have only the interfaces. It is not a problem, we will create a fake search backend that will be enough to test our gui. The other team will test its search backend thoroughly too. We have common interface, it should work. When the other team has finished its backend, we will add a few tests with their backend to check proper integration.

Our Search interface is the following:
searchInterface.h
#ifndef SEARCHINTERFACE_H
#define SEARCHINTERFACE_H

class Query
{
public:
    Query( const char * ) {}
    virtual const char * getValue()=0;
};

class Document
{
public:
    virtual const char * getTitle()=0;
    virtual const char * getAuthor()=0;
    virtual const char * getYear()=0;
};

class Result
{
public:
    virtual int getCount()=0;
    virtual Document * getItem( int i )=0;
};

class Searcher
{
public:
    virtual Result * find( Query * q )=0;
};
#endif // SEARCHINTERFACE_H

A query is built from a text field. The Searcher object returns a result out of a query. The result contain 0 to many documents, which have a title, an author and a year of publication.

Our implementation is very simple. The searcher will return the number of documents that were entered as a number in the query. Each document is numbered and returns tN as title, aN as their author and yN as their year, N being the number of the document in the result.

To write our fake search backend, we add a new test suite, testBackend and we add a creation test. With all the nightmares of C++ when managing pure virtual functions, creating an object is already a difficult task.

testSearchBackend.h
#ifndef TESTSEARCHBACKEND_H
#define TESTSEARCHBACKEND_H

#include "TestCase.h"
#include "TestSuite.h"
#include "TestCaller.h"

class TestSearchBackend : public TestCase
{
public:
    TestSearchBackend(const char * name )
        : TestCase(name)
    {}

    static Test    *suite ()
    {
        TestSuite *testSuite = new TestSuite ("Search Backend");

        testSuite->addTest( new TestCaller<TestSearchBackend> ("testCreate", &TestSearchBackend::testCreate ));

        return testSuite;
    }

protected:
    // My tests
    void testCreate();
};



testSearchBackend.cpp
void TestSearchBackend::testCreate()
{
    FakeSearcher * s = new FakeSearcher();
    FakeQuery * q = new FakeQuery("");
    FakeResult * r = new FakeResult();
    FakeDocument * d = new FakeDocument(0);
    s = 0L;
    q = 0L;
    r = 0L;
    d = 0L;
}

To make it compile, we add our FakeBackend classes:
FakeBackend.h
#ifndef SEARCHBACKEND_H
#define SEARCHBACKEND_H

#include "searchInterface.h"

class FakeQuery : virtual public Query
{
public:
    FakeQuery( const char *s ) : Query(s) {}
    virtual const char * getValue() { return 0L; }
};

class FakeDocument : public Document
{
public:
    FakeDocument(int) {}
    virtual const char * getTitle() { return 0L; }
    virtual const char * getAuthor() { return 0L; }
    virtual const char * getYear() { return 0L; }
};

class FakeResult : public Result
{
public:
    virtual int getCount() { return 0L; }
    virtual Document * getItem( int i ) { return 0L; }
};

class FakeSearcher : virtual public Searcher
{
public:
    FakeSearcher();
    virtual Result * find( Query * q );
};

#endif // SEARCHBACKEND_H


Note that to avoid virtual table compilation errors, every function of the Fake classes must be implemented.

We must add the new suite to our test runner:

mainWindow.cpp extract
void MainWindow::runTestDialog()
{
    GuiTestRunner * runner = new GuiTestRunner;

    // add your test suites here
    Test * suite;
   
    suite = TestSearchDialog::suite();
    runner->addTest( suite->toString(), suite );

    suite = TestSearchBackend::suite();
    runner->addTest( suite->toString(), suite );

    runner->show();
}


Compiles, run. The test passes. Yep!

Now, we will add a second test to our testBackend suite, to check that the backend behaves the way it should:

testSearchBackend.cpp extract
void TestSearchBackend::testBackend()
{
    qDebug("testbackend");
    Query * q = new FakeQuery("1");
    checkEquals( q->getValue(), "1" );

    Document * d = new FakeDocument(1);
    checkEquals( d->getAuthor(), "a1" );
    checkEquals( d->getYear(), "y1" );

    Result * r = new FakeResult( 2 );
    checkEquals( r->getCount(), 2 );
    checkEquals( r->getItem(1)->getAuthor(), "a1" );
    checkEquals( r->getItem(2)->getYear(), "y2" );

    Searcher * s = new FakeSearcher();
    r = s->find( new FakeQuery( "2" ) );
    check( r != 0L );
    checkEquals( r->getCount(), 2 );
    checkEquals( r->getItem(1)->getAuthor(), "a1" );
    checkEquals( r->getItem(2)->getYear(), "y2" );
}

Note that since the function returning our test suite is inline, the new test won't appear until we recompile mainWindow.cpp (where the real code is). This is the cost for having the test suite construction and the test list at the same place.

Since we focus on the test aspect of the application, I find tedious the necessity to click on the menu entry to launch the test suite. So I just replace our mainwidget by our test suite in main.cpp:

main.cpp
#include <qapplication.h>

#include "mainWindow.h"
#include "guiRunner.h"
#include "testSearchDialog.h"
#include "testSearchBackend.h"

int main( int ac, char ** av )
{
    QApplication a( ac, av );

    /*
    MainWindow * mw = new MainWindow( "MainWindow" );
    a.setMainWidget( mw );
    mw->show();
    */

    GuiTestRunner * runner = new GuiTestRunner;

    // add your test suites here
    Test * suite;
   
    suite = TestSearchDialog::suite();
    runner->addTest( suite->toString(), suite );

    suite = TestSearchBackend::suite();
    runner->addTest( suite->toString(), suite );

    runner->show();

    a.exec();
    return 0;
}


I could also have used a timer to call the slot runTestDialog when the main window is built.

We add the various thing necessary to make our Search Backend tests work:

searchBackend.h
class FakeQuery : virtual public Query
{
public:
    FakeQuery( const char *s ) : Query(s), _value(s) {}
    virtual const char * getValue() { return _value.latin1(); }
protected:
    QString _value;
};

class FakeDocument : public Document
{
public:
    FakeDocument(int i);
    virtual const char * getAuthor() { return _author; }
    virtual const char * getYear() { return _year; }
protected:
    char _author[10];
    char _year[10];
};

class FakeResult : public Result
{
public:
    FakeResult( int i=0 ) : _count(i) {}
    virtual int getCount() { return _count; }
    virtual Document * getItem( int i ) { return new FakeDocument(i); }
protected:
    int _count;
};

class FakeSearcher : virtual public Searcher
{
public:
    virtual Result * find( Query * q );
};



searchBackend.cpp:
FakeDocument::FakeDocument(int i)
{
    strcpy( _author, "a0" );
    _author[1] = '0' + i;
    strcpy( _year, "y0" );
    _year[1] = '0' + i;
}

Result * FakeSearcher::find( Query * q)
{
    if (q == 0L) return 0L;
    return new FakeResult( atoi( q->getValue() ) );
}


With this, all our tests pass at 100%. We are ready for the next step.


Connecting Search frontend to Search backend

Now, we have a workind backend. We have a frontend to make search queries. So we need to interconnect them. The things we must do are:

- we must assiociate a Searcher object with our dialog
- we must connect the search button with a search action
- we must fill the result widget with the result of a search
- we must check the result widget content. It is usually a good idea to check
  for empty results, results with 1 document and results with many documents.
- we must check that the dialog is correctly reinitialised between two
  queries.


Assiociate a searcher to our Search Dialog

Okay, we will begin our first test: add a Searcher object to the dialog. Our test is

testSearchDialog.cpp extract
void TestSearchDialog::testDialogSearcher()
{
    check( _sd->getSearcher() == 0L );
    Searcher * s = new FakeSearcher();
    _sd->setSearcher( s );
    check( _sd->getSearcher() == s );
    Searcher * s2 = new FakeSearcher();
    check( _sd->getSearcher() == s2 );

}


To make the test compile, I add the functions to searchDialog.h:

searchDialog.h
class SearchDialog : public QDialog
{
public:
    SearchDialog();

    void setSearcher( Searcher * s ) { _searcher = s; }
    Searcher * getSearcher() { return _searcher; }

protected:
    QLabel * _queryLabel;
    QLineEdit * _queryText;
    QPushButton * _buttonSearch;
    QListView * _resultWidget;
    Searcher * _searcher;

friend TestSearchDialog;
};


The test compiles. I touch main.cpp to make it include the new test, I run, the first test fails. Gcc hasn't set _searcher to 0.  No problem, I'll set it myself in the SearchDialog constructor.

searchDialog.cpp
SearchDialog::SearchDialog()
: QDialog(0, "Search Dialog", false)
{
    _searcher = 0L;

...

Now, the test pass.

Our first query

We will  now make our first query, expecting one object in the result. We set the text in the text field to "1". We click the button and check that the result widget is filled. To simulate the button clicking, Qt has provided us with a animageClicking() function for QPushButton.

Our test:
testSearchDialog.cpp extract
void TestSearchDialog::testQuery1()
{
    _sd->_queryText->setText("1");
    _sd->_buttonSearch->animateClick();
    checkEquals( _sd->_resultWidget->childCount(), 1 );
}


The test compiles. When we run it, we see the window of the search dialog popping up. The test doesn't pass. To fix it, we must connect the clicked() signal of the button to a function that will actually fill the result widget. Go!

Our new search dialog:

searchDialog.cpp extract
SearchDialog::SearchDialog()
: QDialog(0, "Search Dialog", false)
{
    _searcher = 0L;

    QVBoxLayout * vly = new QVBoxLayout( this );
    vly->setAutoAdd( true );

    QHBox * hbox = new QHBox( this );
    _queryLabel = new QLabel( "Query : ", hbox );
    _queryText = new QLineEdit( hbox );
    _buttonSearch = new QPushButton( "Search", hbox );
    _resultWidget = new QListView( this );

    _resultWidget->addColumn( "Title" );
    _resultWidget->addColumn( "Author" );
    _resultWidget->addColumn( "Year" );
   
    connect( _buttonSearch, SIGNAL(clicked()), SLOT(performSearch()) );
}

void SearchDialog::performSearch()
{
    if (_searcher == 0L) {
        qDebug("SearchDialog::performSearch() - No searcher set!" );
        return;
    }

    FakeQuery * q = new FakeQuery( _queryText->text().latin1() );
    Result * r = _searcher->find( q );
    for( int i=0; i < r->getCount(); i++) {
        Document * d = r->getItem(i);
        new QListViewItem( _resultWidget, d->getTitle(), d->getAuthor(),
                d->getYear() );
    }
}


To make it work, it turns out that we need to give Qt some time to process the signals. We also had forgotten to set the searcher. Our test becomes :

testSearchDialog.cpp extract
void TestSearchDialog::testQuery1()
{
    _sd->setSearcher( new FakeSearcher() );
    _sd->_queryText->setText("1");
    _sd->_buttonSearch->animateClick();

    QTime t;
    t.start();
    while( t.elapsed() < 1000 ) {
        qApp->processEvents();
    }

    checkEquals( _sd->_resultWidget->childCount(), 1 );
}


This works. We refine the test a bit.
testSearchDialog.cpp extract
void TestSearchDialog::testQuery1()
{
    _sd->setSearcher( new FakeSearcher() );
    _sd->_queryText->setText("1");
    _sd->_buttonSearch->animateClick();

    QTime t;
    t.start();
    while( t.elapsed() < 1000 ) {
        qApp->processEvents();
    }

    checkEquals( _sd->_resultWidget->childCount(), 1 );
    QListViewItem * item;
    item = _sd->_resultWidget->firstChild();
    checkEquals( item->text(0), "t1" );
    checkEquals( item->text(1), "a1" );
    checkEquals( item->text(2), "y1" );
}


It works too. Let's go to next step.

An empty result

Now we do a query that returns an emtpy result and check that everything is ok. Once we have written a test, the next is very quickly written.

testSearchDialog.cpp extract
void TestSearchDialog::testQuery0()
{
    _sd->setSearcher( new FakeSearcher() );
    _sd->_queryText->setText("0");
    _sd->_buttonSearch->animateClick();

    QTime t;
    t.start();
    while( t.elapsed() < 1000 ) {
        qApp->processEvents();
    }

    checkEquals( _sd->_resultWidget->childCount(), 0 );
}

The test runs perfectely right from the start. Good.

A query with multiple results

Our test:

testSearchDialog.cpp extract
void TestSearchDialog::testQueryMany()
{
    _sd->setSearcher( new FakeSearcher() );
    _sd->_queryText->setText("5");
    _sd->_buttonSearch->animateClick();

    QTime t;
    t.start();
    while( t.elapsed() < 1000 ) {
        qApp->processEvents();
    }

    checkEquals( _sd->_resultWidget->childCount(), 5 );
    QListViewItem * item;
    item = _sd->_resultWidget->firstChild();
    checkEquals( item->text(0), "t1" );
    checkEquals( item->text(1), "a1" );
    checkEquals( item->text(2), "y1" );
    item = item->nextSibling();
    checkEquals( item->text(0), "t2" );
    item = item->nextSibling();
    checkEquals( item->text(1), "a3" );
    item = item->nextSibling();
    checkEquals( item->text(2), "y4" );
    item = item->nextSibling();
    checkEquals( item->text(0), "t5" );
    item = item->nextSibling();
    check( item == 0L );
}


It runs right from the start! We wrote good code. :-)

Multiple queries

Does it work with multiple queries ? We just check:

testSearchDialog.cpp extract
void TestSearchDialog::testMultipleQuery()
{
    testQueryMany();
    testQuery1();
}

It doesn't work, because we didn't clean the result widget when performing a new search.

Adding a _resultWidget->clean() in performWidget makes the test work:

searchDialog.cpp extract
void SearchDialog::performSearch()
{
    if (_searcher == 0L) {
        qDebug("SearchDialog::performSearch() - No searcher set!" );
        return;
    }

    _resultWidget->clear();

    FakeQuery * q = new FakeQuery( _queryText->text().latin1() );
    Result * r = _searcher->find( q );
    for( int i=0; i < r->getCount(); i++) {
        Document * d = r->getItem(i+1);
        new QListViewItem( _resultWidget, d->getTitle(), d->getAuthor(),
                d->getYear() );
    }
    delete q;
    delete r;
}


Conclusion

We now have a working search dialog, but more importantly, we are sure that our search dialog works with our search backend. It would have been possible to code the search dialog without any tests. It would have taken less time. But if you had done that, you could have only supposed that your search dialog worked. You would click here and there and check by hand the result. Thank to our automated tests, we are _sure_ that it works the way it should in every situation. And if you ever break anything, our tests will report it immediately.

Qt makes it possible to simulate all the user actions. Usually there is already a function like our QPushButton::animateClick() to perform manually the user action. But if no such function exist, you can always pass some QMouseEvent to move the cursor and click on buttons or whatever.

For any feedback, contact me : Philippe Fremy <phil at freehackers.org>

Last modification : $Date: 2004/06/02 07:58:38 $ - $Author: philippe $