Unit test of a custom QSpinBox

This article provides a compact introduction into unit testing and demonstrates some of the testing principles on a custom class CustomSpinBox which was implemented in this article. In the class CustomSpinBox we had to override the function stepBy(int) to allow an increment that follows a geometric sequence, i.e. 1, 2, 4, 8, 16 etc.

A minimal introduction into unit testing

Before you attempt any unit testing you should know the following:

  • With a unit test we are trying to see whether the result of a function given a certain input matches the desired output.
  • Each class requires at least one test class. Each method requires at least one test method.
  • Tests should be kept separate from the production code. The user does not need the test code.
For a proper introduction I suggest the book of Roy Osherove: The Art of Unit Testing: With Examples in .NET. The book is written for .NET, but you should give it a chance, the principles are explained extremely well. Another book to have a look at is Johan Thelin's Foundations of Qt Development which also contains a chapter on unit testing in Qt.

In the following few paragraphs I am going to demonstrate how to unit test the class CustomSpinBox. Please note, that I am only going to test the overidden function CustomSpinBox::stepBy(int). If you are interested in the test of the original QSpinBox, you can find it in the above mentioned book by Johan Thelin.

Summary of the CustomSpinBox implementation

Before I jump into unit testing, let me summarize the CustomSpinBox and the functionality of the overridden stepBy(int) method. The method stepBy(int) is called whenever the user presses the arrow up or arrow down of the spinbox.

This way we change the values of the spinbox in a geometric sequence 1, 2, 4, 8, 16 etc. Please note that the parameter of stepBy(int), i.e. 1 or -1, indicates the direction, i.e. whether the arrow up or arrow down was pressed. It does not indicate the value of the increment. The customspinbox.h and customspinbox.cpp can be seen below:

#ifndef CUSTOMSPINBOX_H
#define CUSTOMSPINBOX_H

#include <QSpinBox>

class CustomSpinBox : public QSpinBox
{
    Q_OBJECT
public:
    CustomSpinBox(QWidget* parent = 0);
public slots:
    void stepBy(int steps);
};

#endif // CUSTOMSPINBOX_H
#include "customspinbox.h"

CustomSpinBox::CustomSpinBox(QWidget* parent) : QSpinBox(parent){
}

void CustomSpinBox::stepBy(int steps)
{
  if(steps == 1)
      setValue(value()*2);
  else if(steps == -1)
      setValue(value()/2);
  else
      QSpinBox::stepBy(steps);
}

How to create a separate test project

As I mentioned before, the production code should be separated from the test code. To separate your test project from the production code you should do the following:

  1. Create a new project called testcustomspinbox. Place the test project into the same folder as the production project (i.e. make them siblings). If you work with QtCreator, I suggest that you create an empty Qt project, rather Qt Unit Test project, as for the latter you need to understand the details of testing.
  2. Add 'QT += testlib' into the testcustomspinbox.pro.
  3. Add a new class TestCustomSpinBox into your testcustomspinbox project, i.e. add testcustomspinbox.h and testcustomspinbox.cpp. For the moment leave it empty.
  4. Add main.cpp into the project. Leave it empty.
  5. Make sure that your project file testcustomspinbox.pro changed to the following:
  6. QT       += testlib
    
    HEADERS += \
        testcustomspinbox.h
    
    SOURCES += \
        main.cpp \
        testcustomspinbox.cpp
    
  7. Link the production project to the test project, i.e. add the path to the production project with INCLUDEPATH and the path to the individual header and the source file of the class under test. If you placed the production project 'customspinbox' into the same folder as the test project 'testcustomspinbox', you should change the testcustomspinbox.pro to the following:
  8. QT       += testlib
    
    INCLUDEPATH += ../customspinbox
    
    HEADERS += \
        testcustomspinbox.h \
        ../customspinbox/customspinbox.h
    
    SOURCES += \
        main.cpp \
        testcustomspinbox.cpp \
        ../customspinbox/customspinbox.cpp
    

Basic implementation of the test class - without data set

Now it's time to implement the test class TestCustomSpinBox. Open the empty files testcustomspinbox.h and testcustomspinbox.cpp and insert the implementation that you see below. Notice what distinguishes a test class from a regular class:

#ifndef TESTCUSTOMSPINBOX_H
#define TESTCUSTOMSPINBOX_H

#include <QObject>
#include <QTest>
#include "customspinbox.h"

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

private slots:
    void testStepBy();
};

#endif // TESTCUSTOMSPINBOX_H

The source file and the implementation of the function testStepBy() follows. Scan through it and then look at the explanations below.

#include "testcustomspinbox.h"

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

void TestCustomSpinBox::testStepBy(){
    CustomSpinBox csb;
    csb.setRange(1,256);

    csb.setValue(4);
    csb.stepBy(1);
    QCOMPARE(csb.value(),8);

    csb.setValue(4);
    csb.stepBy(-1);
    QCOMPARE(csb.value(),2);

    // test of the lower limit
    csb.setValue(1);
    csb.stepBy(-1);
    QCOMPARE(csb.value(),1);

    // test of the upper limit
    csb.setValue(256);
    csb.stepBy(1);
    QCOMPARE(csb.value(),256);
}
  1. We construct an instance of CustomSpinBox, set its range to (1,256). and its value to 4. You can choose different values, of course.
  2. CustomSpinBox csb;
    csb.setRange(1,256);
    csb.setValue(4);
    
  3. We call the function stepBy(1), (equivalent to pressing the arrow up in the spinbox) which should multiply the original value by 2, i.e. the result should be 4*2=8. Please, note that the parameter of the stepBy(int) function, i.e. 1, does not enter the computation, it only indicates the direction, i.e whether the arrow up or arrow down was pressed. To check the result we call the Qt macro QCOMPARE(actual value in the spinbox, expected value in the spinbox).
  4. csb.stepBy(1);
    QCOMPARE(csb.value(),8);
    
  5. To check whether the mechanism works also when pressing the arrow down we reset the value of the spinbox to 4 . We call stepBy(-1), (equivalent to pressing the arrow down in the spinbox). This should divide the original value 4 by 2, i.e. the result should be 2. With the QCOMPARE macro we check whether the result is correct.
  6. csb.setValue(4);
    csb.stepBy(-1);
    QCOMPARE(csb.value(),2);
    
  7. After that we perform two additional tests. We check that the function stepBy(int) does not increment the value in the spinbox once its upper limit 256 was reached and does not decrement the value at its lower limit 1.

Now, let's move to the main.cpp. To test a gui widget such as a spinbox we have to create an instance of QApplication. After that we construct an instance of the test class TestCustomSpinBox and run the test by calling QTest::qExec().

#include <QApplication>
#include "testcustomspinbox.h"

int main(int argc, char *argv[])
{
    // so that the gui widgets can be tested
    QApplication a(argc, argv);

    TestCustomSpinBox tSpinBox;
    return QTest::qExec(&tSpinBox, argc, argv);
}

That's it! If you run the code and everything works correctly, you should see the following output:

********* Start testing of TestCustomSpinBox *********
Config: Using QTest library 4.8.0, Qt 4.8.0
PASS : TestCustomSpinBox::initTestCase()
PASS : TestCustomSpinBox::testStepBy()
PASS : TestCustomSpinBox::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of TestCustomSpinBox *********

The implementation of the test class with data set

The same test can be performed in a more sophisticated way with the use of a data set. This will require some changes to the header and the source file. In the header please notice the extra data function testStepBy_data(). This function has to have the same name as the test function appended by '_data()'.

#ifndef TESTCUSTOMSPINBOX_H
#define TESTCUSTOMSPINBOX_H

#include <QObject>
#include <QTest>
#include "customspinbox.h"

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

private slots:
    void testStepBy();
    void testStepBy_data();

};
#endif // TESTCUSTOMSPINBOX_H

What the function testStepBy_data() represents is a test table that looks like the one below. This is in fact just a different way of representing the original test.

startValue direction endValue
Move Up 4 1 8
Move Down 4 -1 2
Move Down Limit 1 -1 1
Move Up Limit 256 1 256

In the source code, such a table would be constructed and populated with the use of functions QTest::addColumn() and QTest::newRow().

void TestCustomSpinBox::testStepBy_data(){
    QTest::addColumn ("startValue");
    QTest::addColumn ("direction");
    QTest::addColumn ("endValue");

    QTest::newRow("Move Up") << 4 << 1 << 8;
    QTest::newRow("Move Down") << 4 << -1 << 2;

    QTest::newRow("Move Down Limit") << 1 << -1 << 1;
    QTest::newRow("Move Up Limit") << 256 << 1 << 256;
}

The next thing to implement is the testStepBy() method. You can see it below. First, we construct an instance of the CustomSpinBox and set its range. After that we deal with the test data. Notice the QFETCH macro. The macro creates a local variable 'int startValue', 'int direction' and 'int endValue'. These local variables get populated with the data from the test table located within the testStepBy_data(). After we fetch the 'startValue' and 'direction' from the data table, we set the value of the spinbox to the 'startValue' and increment it by calling stepBy(direction). Next, we fetch the expected 'endValue' from the testStepBy_data(). Last, we compare the actual value in the spinbox to the expected endValue.

void TestCustomSpinBox::testStepBy(){
    CustomSpinBox csb;
    csb.setRange(1,256);

    QFETCH(int, startValue);
    QFETCH(int, direction);
    csb.setValue(startValue);
    csb.stepBy(direction);
    QFETCH(int, endValue);
    QCOMPARE(csb.value(), endValue);
}

There won't be any changes to the main.cpp. You can run it just the same way you ran the project without the data set.


Latest update: 09.07.2015
Created: 2014
© Walletfox.com, 2017