Development/Tutorials/Unittests (ru)

From KDE TechBase


Автор: Брэд Хардс, Sigma Bravo Pty Limited
Перевод: Mixolap

Перевод статьи не завершён, см. qtfaq.ru

Введение

Эта статья посвящена написанию unit-тестов для Qt4 и KDE4 на основе инфраструктуры QtTestLib, появившейся в Qt4.1. Здесь описываются основные принципы unit-тестирования, обучающий материал по инфраструктуре QtTestLib и рекомендации для получения наибольшей отдачи от ваших усилий. При написании этой статьи использовалась Qt 4.1 и KDE 4.

О юнит-тестировании

Юнит-тест – это тест, который проверяет функциональность, поведение и корректность отдельного программного компонента. В коде Qt4 (включая код KDE4) юнит-тесты почти везде используются для тестирования отдельных классов С++ (хотя тестирование макросов или функций С также возможно).

Юнит-тесты – ключевая часть разработки ведомой тестированием (Test Driven Development), поскольку они используются во всех процессах разработки. Покрытие всего кода тестами не является существенным (хотя очевидно, что это очень желательно!). Каждый отдельный тест – хороший шаг по улучшению качества кода.

Надо отметить, что юнит-тесты – это в большей степени динамические тесты (то есть они запускаются, используя скомпилированный код) чем статические тесты по анализу (которые выполняются над исходником или его промежуточным представлением).

К тому же, большинство программистов пишут «одноразовый» код, который они используют для проверки реализации, не называя его «юнит-тестами». Если этот код проверен и встроен в систему разработки, то он подходит для проверки реализации так же хорошо. QtTestLib просто предоставляет инфраструктуру делающую эту работу более лёгкой и эффективной.

Надо отметить, что иногда заманчиво обращаться к юнит-тестам как к чисто проверочному инструменту. Кроме того, что юнит-тесты помогают удостовериться в правильности функционирования и поведения, он также помогают в других аспектах повышения качества кода. Написание юнит-тестов требует немного отличного подхода к написанию кода класса и продумывания с какими входными параметрами должны быть тесты, чтобы помочь определить логические недостатки в коде (до того как тесты запущены). К тому же, создание тестируемого кода приносит другую пользу, ведь они требуют модульности программы, т.е. чтобы «переплетение» классов было минимальным (close coupling). В любом случае, достаток концептуального материала позволяет говорить о специфическом инструменте, который уменьшит затраты и позволит нам выполнить свою работу.

О QtTestLib

QtTestlib – легковесная тестирующая библиотека разработанная «тролями» и реализована под GPL (коммерческая лицензия также доступна). Написана на С++ и является кроссплатформенной. Поставляется как часть инструментария включенного в Qt 4.1, а в более ранних версиях поставляется отдельно (ранние версии имеют различный API и приведенные здесь примеры не будет работать в них). В дополнение к обычным возможностям юнит-тестирования QtTestLib также позволяет создавать простые тесты графического интерфейса, основанного на посылке событий (QEvent). Это поможет вам тестировать виджеты, но полного покрытия графической чатис приложения достичь будет сложно. Каждый тестовый случай – отдельное тестирующее приложение. В отличие от CppUni или JUni, здесь нет запускающего класса. На самом деле, каждый тестовый случай является исполяемым файлом, который запускается обычным путём.

Руководство 1: простой тест класса даты.

В этом руководстве, мы сделаем простой тест для класса, который представляет дату, используя QtTestLib как каркас для тестирования. Опуская детали как класс даты работает, мы просто воспользуемся классом QDate из поставки Qt. В реальности же, в юнит-тесте, вы будете тестировать код, который написан вами или вашими коллегами. Код ниже представляет собой целый тестовый случай:

#include <QtTest>
include <QtCore>

class testDate: public QObject
{
    Q_OBJECT
private slots:
    void testValidity();
    void testMonth();
};
 
void testDate::testValidity()
{
    // 11 March 1967
    QDate date( 1967, 3, 11 );
    QVERIFY( date.isValid() );
}
 
void testDate::testMonth()
{
    // 11 March 1967
    QDate date;
    date.setYMD( 1967, 3, 11 );
    QCOMPARE( date.month(), 3 );
    QCOMPARE( QDate::longMonthName(date.month()),
              QString("March") );
}
 
 
QTEST_MAIN(testDate)
#include "tutorial1.moc"

Разберём по строкам. Первая строка импортирует заголовочные файлы пространства имен QtTest. Вторая строка импортирует заголовки QtCore (большой необходимости в этом нет, так как QtTest также импортирует их, но для безопасности можно). Строки с 4 по 10 содержат класс теста testDate. Отметим, что testDate наследуется от QObject и имеет макрос Q_OBJECT – QtTestLib требует специфичной функциональности Qt, которая представлена в Qobject.

Строки с 12 по 17 реализуют наш первый тест, который проверяет дату на правильность. Отметим, что используется QVERIFY макрос, который проверяет, что условие возвращает true. Таким образом, если date.isValid() возвращает правда, то тест проходит, иначе тест проваливается. QVERIFY аналогично ASSERT других тестовых пакетах. Аналогично строки с 19 по 27 реализуют другой тест, который проверяет установку даты и пару доступов к функциям. В этом случае мы используем QCOMPARE, которое проверяет, что условия равны. Таким образом, если date.month() вернет 3, то это будет означать, что эта часть тестов прошла, иначе тест будет провален с сообщением:

Warning

As soon as a QVERIFY evaluates to false or a QCOMPARE does not have two equal values, the whole test is marked as failed and the next test will be stated. So in the example above, if the check at line 24 fails, then the check at lines 25 and 26 will not be run.

В следующем упражнении мы посмотрим, как работать с проблемами, к которым такое поведение может привести. Строка 30 использует QTEST_MAIN который создает для нас точку входа в программу с соответствующим вызовом testDate класса юнит-теста. Строка 31 включает результаты работы компилятора Мета-Объекта, таким образом мы можем использовать функциональность QObject. Файл проекта, который соответствует приведенному коду показан ниже. Вы можете использовать qmake для создания Makefile и затем откомпилировать его с make.

Пример 2. проект QDate юнит-теста

CONFIG += qtestlib
TEMPLATE = app
TARGET += 
DEPENDPATH += .
INCLUDEPATH += .
 
# Input
SOURCES += tutorial1.cpp

Это довольно обычный файл проекта, за исключением добавления:

CONFIG += qtestlib

Это добавляет пути к заголовочным файлам и библиотекам в Makefile Таким образом мы получим приложение, которое может быть запущено из командной строки. Результат должен получиться похожим на следующий:
Пример 3. Результат QDate юнит-теста.

$ ./tutorial1
********* Start testing of testDate *********
Config: Using QTest library 4.1.0, Qt 4.1.0-snapshot-20051003
PASS   : testDate::initTestCase()
PASS   : testDate::testValidity()
PASS   : testDate::testMonth()
PASS   : testDate::cleanupTestCase()
Totals: 4 passed, 0 failed, 0 skipped
********* Finished testing of testDate *********

Глядя на результаты, вы можете увидеть, что они включают версию библиотеки тестов и самой Qt, а также статус каждого запущенного теста. В дополнении к тестам, которые мы определили - testValidity и testMonth, здесь также присутствуют процедуры инициализации и очистки, которые могут быть использованы для дополнительной конфигурации при необходимости.

Провал тестов

Если мы сделали ошибку в коде реализации или в коде юнит-теста, то результатом будет ошибка. Пример показан ниже: Пример 4. Результат провала юнит-теста QDate

$ ./tutorial1
********* Start testing of testDate *********
Config: Using QTest library 4.1.0, Qt 4.1.0-snapshot-20051003
PASS   : testDate::initTestCase()
PASS   : testDate::testValidity()
FAIL!  : testDate::testMonth() Compared values are not the same
   Actual (date.month()): 4
   Expected (3): 3
    Loc: [tutorial1.cpp(25)]
PASS   : testDate::cleanupTestCase()
Totals: 3 passed, 1 failed, 0 skipped
********* Finished testing of testDate *********

Запуск отдельных тестов

Когда количество тестовых функций увеличивается и некоторые функции выполняются долго, будет удобно запускать только выбранную функцию. На пример, если вы хотите запустить только функцию testValidity, то вы просто укажите ее в командной строке, как показано ниже:

Example 5. Результат выполнения юнит-теста QDate для отдельной функции

$ ./tutorial1 testValidity
********* Start testing of testDate *********
Config: Using QTest library 4.1.0, Qt 4.1.0-snapshot-20051003
PASS   : testDate::initTestCase()
PASS   : testDate::testValidity()
PASS   : testDate::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of testDate *********

Отметим, что функции initTestCase и cleanupTestCase выполняются всегда, таким образом инициализация и очистка будет выполнена. Вы можете получить список доступных функций опцией -functions, как показано ниже:

Example 6. QDate unit test output - listing functions

$ ./tutorial1 -functions
testValidity()
testMonth()

Опции детализации результатов

Вы можете увеличить детализацию результатов используя опции -v1, -v2 и -vs. -v1 предоставляет сообщение на входе в каждую тестовую функцию. Такое будет полезно, например, при определении места зацикливания теста. Это показано ниже:

Пример 7. Результат юнит-теста QDate — детализация результатов

$ ./tutorial1 -v1
********* Start testing of testDate *********
Config: Using QTest library 4.1.0, Qt 4.1.0-snapshot-20051003
INFO   : testDate::initTestCase() entering
PASS   : testDate::initTestCase()
INFO   : testDate::testValidity() entering
PASS   : testDate::testValidity()
INFO   : testDate::testMonth() entering
PASS   : testDate::testMonth()
INFO   : testDate::cleanupTestCase() entering
PASS   : testDate::cleanupTestCase()
Totals: 4 passed, 0 failed, 0 skipped
********* Finished testing of testDate *********

Опция -v2 показывает вызов каждого макроса QVERIFY, QCOMPARE и QTEST, так же хорошо, как и вход в каждую функцию. Это будет полезно, например, чтобы убедиться, что каждый шаг был выполнен. Это показано ниже:

Пример 8. Результат юнит-теста QDate — более детализированный

$ ./tutorial1 -v2
********* Start testing of testDate *********
Config: Using QTest library 4.1.0, Qt 4.1.0-snapshot-20051003
INFO   : testDate::initTestCase() entering
PASS   : testDate::initTestCase()
INFO   : testDate::testValidity() entering
INFO   : testDate::testValidity() QVERIFY(date.isValid())
    Loc: [tutorial1.cpp(17)]
PASS   : testDate::testValidity()
INFO   : testDate::testMonth() entering
INFO   : testDate::testMonth() COMPARE()
    Loc: [tutorial1.cpp(25)]
INFO   : testDate::testMonth() COMPARE()
    Loc: [tutorial1.cpp(27)]
PASS   : testDate::testMonth()
INFO   : testDate::cleanupTestCase() entering
PASS   : testDate::cleanupTestCase()
Totals: 4 passed, 0 failed, 0 skipped
********* Finished testing of testDate *********

Опция -vs показывает каждый сгенерированный сигнал. В нашем примере сигналы отсутствют, так что от -vs эффекта не будет. Получение списка сигналов полезно для отладки проваленных тестов, особенно графических тестов, которые мы посмотрим в третьем руководстве.

Вывод в файл

Если вы хотите направить вывод результатов тестирования в файл, вы можете использовать опцию -o имя_файла, где имя_файла замените на имя файла, в который вы хотите сохранить вывод.

Руководство 2: Управляемое данными тестирование класса даты.

В предыдущем примере, мы рассмотрели как мы можем тестировать класс даты. Если мы решили, что мы действительно нуждаемся в тестировании большего количества дат, мы должны будем скопировать и вставить большое количество кода. Если мы потом заменим имя функции, то придется его поменять во многих местах. Как альтернативное решение данного типа проблем в наших тестах, QtTestLib предлагает поддержку тестирования управляемое данными. Простейший пример для понимания тестирования управляемого данными приведен в примере показанном ниже:

Пример 9. Код теста QDate, версия управляемая данными

#include <QtTest>
#include <QtCore>
 
 
class testDate: public QObject
{
    Q_OBJECT
private slots:
    void testValidity();
    void testMonth_data();
    void testMonth();
};
 
void testDate::testValidity()
{
    // 12 March 1967
    QDate date( 1967, 3, 12 );
    QVERIFY( date.isValid() );
}
 
void testDate::testMonth_data()
{
    QTest::addColumn<int>("year");  // the year we are testing
    QTest::addColumn<int>("month"); // the month we are testing
    QTest::addColumn<int>("day");   // the day we are testing
    QTest::addColumn<QString>("monthName");   // the name of the month
 
    QTest::newRow("1967/3/11") << 1967 << 3 << 11 << QString("March");
    QTest::newRow("1966/1/10") << 1966 << 1 << 10 << QString("January");
    QTest::newRow("1999/9/19") << 1999 << 9 << 19 << QString("September");
    // more rows of dates can go in here...
}
 
void testDate::testMonth()
{
    QFETCH(int, year);
    QFETCH(int, month);
    QFETCH(int, day);
    QFETCH(QString, monthName);
 
    QDate date;
    date.setYMD( year, month, day);
    QCOMPARE( date.month(), month );
    QCOMPARE( QDate::longMonthName(date.month()), monthName );
}
 
 
QTEST_MAIN(testDate)
#include "tutorial2.moc"

Как вы должны были заметить, мы добавили новый метод testMonth_data и перенесли тест определенной даты из testMonth. Мы добавили немного больше кода (который будет пояснен далее), но в результате мы отделили данные от кода, который мы используем для теста. Важны имена функций, вы должны указать суффикс _data для функции установки данных, и первая часть функции установки данных должна соответствовать наименованию управляющей процедуры.

Для использования данных мы просто вызываем QFETCH доя получения соответствующих данных из каждого ряда. Аргументы для QFETCH — тип переменной и наименование столбца (которое соответствует имени локальной переменной в которую извлекается значение). Вы можете использовать эти данные в QCOMPARE или QVERIFY. Код запускается для каждого набора параметров, что можно увидеть ниже:

Пример 10. Результаты тестирования, демонстрация QFETCH

$ ./tutorial2 -v2
********* Start testing of testDate *********
Config: Using QTest library 4.1.0, Qt 4.1.0-snapshot-20051020
INFO   : testDate::initTestCase() entering
PASS   : testDate::initTestCase()
INFO   : testDate::testValidity() entering
INFO   : testDate::testValidity() QVERIFY(date.isValid())
   Loc: [tutorial2.cpp(19)]
PASS   : testDate::testValidity()
INFO   : testDate::testMonth() entering
INFO   : testDate::testMonth(1967/3/11) COMPARE()
   Loc: [tutorial2.cpp(44)]
INFO   : testDate::testMonth(1967/3/11) COMPARE()
   Loc: [tutorial2.cpp(45)]
INFO   : testDate::testMonth(1966/1/10) COMPARE()
   Loc: [tutorial2.cpp(44)]
INFO   : testDate::testMonth(1966/1/10) COMPARE()
   Loc: [tutorial2.cpp(45)]
INFO   : testDate::testMonth(1999/9/19) COMPARE()
   Loc: [tutorial2.cpp(44)]
INFO   : testDate::testMonth(1999/9/19) COMPARE()
   Loc: [tutorial2.cpp(45)]
PASS   : testDate::testMonth()
INFO   : testDate::cleanupTestCase() entering
PASS   : testDate::cleanupTestCase()
Totals: 4 passed, 0 failed, 0 skipped
********* Finished testing of testDate *********

Макрос QTEST

Как альтернативу использованию QFETCH и QCOMPARE, вы можете использовать макрос QTEST. QTAKES принимает два аргумента, и если один из них строка, он ищет строку как аргумент в текущем наборе переменных. Ниже вы можете увидеть как это может быть использовано, что является эквивалентом коду testMonth() в предыдущем примере.

Пример 11. Код теста QDate, версия, использующая QTEST

void testDate::testMonth()
{
    QFETCH(int, year);
    QFETCH(int, month);
    QFETCH(int, day);
 
    QDate date;
    date.setYMD( year, month, day);
    QCOMPARE( date.month(), month );
    QTEST( QDate::longMonthName(date.month()), "monthName" );
}

В приведенном примере, отметим, что monthName заключено в кавычки и мы больше не используем QFETCH для вызова monthName. QCOMPARE также может быть заменено использованием QTEST, но это будет менее эффективно, потому что мы все равно нуждаемся в использовании QFETCH для месяца, чтобы использовать его в функции setYMD.

Запуск отдельных тестов с отдельными данными

В предыдущем руководстве мы увидели, как запускать определенный тест по его имени в командной строке. В тестировании , управляемом данными, вы можете выбрать с какими данными вы хотите запустить тест указав их в параметрах. На пример, если мы хотим запустить тест для первого набора, то мы должны использовать: ./tutorial2 -v2 testMonth:1967/3/11 Результаты приведены ниже:

Пример 12. Результат юнит-теста QDate — выбор конкретного теста с конкретными данными

$ ./tutorial2 -v2 testMonth:1967/3/11
********* Start testing of testDate *********
Config: Using QTest library 4.1.0, Qt 4.1.0-snapshot-20051020
INFO   : testDate::initTestCase() entering
PASS   : testDate::initTestCase()
INFO   : testDate::testMonth() entering
INFO   : testDate::testMonth(1967/3/11) COMPARE()
   Loc: [tutorial2.cpp(44)]
INFO   : testDate::testMonth(1967/3/11) COMPARE()
   Loc: [tutorial2.cpp(45)]
PASS   : testDate::testMonth()
INFO   : testDate::cleanupTestCase() entering
PASS   : testDate::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of testDate *********

Руководство 3: Тестирование графического интерфейса

В предыдущих двух руководствах мы тестировали вызовы управляемые данными. Но всеже Qt и KDE приложения используют графические классы, которые принимают пользовательский ввод (обычно с клавиатуры или мыши). QtTestLib также поддерживает тестирование этих классов, что мы увидим в этом руководстве.

Мы будем использовать существующий класс для нашего тестируемого окружения и и снова он будет связан с датой — будет наследоваться от QDateEdit. Воспользуемся простым виджетом для ввода даты. Скриншот виджета представлен ниже:

Рис.1. Скриншот виджета QDateEdit

QtTestLib обеспечивает тестирование интерфейса путем генерации QInputEvent событий. Для приложения, которое получает эти события, они являются тем же самым, что и обычные нажатие клавиш или щелчки (перетаскивания) мышью. Так как реальные мышь и клавиатура не задействованы, вы можете продолжить использование их в обычном режиме, пока запущен тест. Пример того как вы можете использовать функциональность QtTestLib по тестированию интерфейса представлена ниже.

Пример 13. Тестируемый код QDateEdit.

#include <QtTest>
#include <QtCore>
#include <QtGui>
Q_DECLARE_METATYPE(QDate)
 
class testDateEdit: public QObject
{
    Q_OBJECT
private slots:
    void testChanges();
    void testValidator_data();
    void testValidator();
};
 
void testDateEdit::testChanges()
{
    // 11 March 1967
    QDate date( 1967, 3, 11 );
    QDateEdit dateEdit( date );
 
    // up-arrow should increase day by one
    QTest::keyClick( &dateEdit, Qt::Key_Up );
    QCOMPARE( dateEdit.date(), date.addDays(1) );
 
    // we click twice on the "reduce" arrow at the bottom RH corner
    // first we need the widget size to know where to click
    QSize editWidgetSize = dateEdit.size();
    QPoint clickPoint(editWidgetSize.rwidth()-2, editWidgetSize.rheight()-2);
    // issue two clicks
    QTest::mouseClick( &dateEdit, Qt::LeftButton, 0, clickPoint);
    QTest::mouseClick( &dateEdit, Qt::LeftButton, 0, clickPoint);
    // and we should have decreased day by two (one less than original)
    QCOMPARE( dateEdit.date(), date.addDays(-1) );
 
    QTest::keyClicks( &dateEdit, "25122005" );
    QCOMPARE( dateEdit.date(), QDate( 2005, 12, 25 ) );
 
    QTest::keyClick( &dateEdit, Qt::Key_Tab, Qt::ShiftModifier );
    QTest::keyClicks( &dateEdit, "08" );
    QCOMPARE( dateEdit.date(), QDate( 2005, 8, 25 ) );
}
 
void testDateEdit::testValidator_data()
{
    qRegisterMetaType<QDate>("QDate");
 
    QTest::addColumn<QDate>( "initialDate" );
    QTest::addColumn<QString>( "keyclicks" );
    QTest::addColumn<QDate>( "finalDate" );
 
    QTest::newRow( "1968/4/12" ) << QDate( 1967, 3, 11 )
                                 << QString( "12041968" )
                                 << QDate( 1968, 4, 12 );
 
    QTest::newRow( "1967/3/14" ) << QDate( 1967, 3, 11 )
                                 << QString( "140abcdef[" )
                                 << QDate( 1967, 3, 14 );
    // more rows can go in here
}
 
void testDateEdit::testValidator()
{
    QFETCH( QDate, initialDate );
    QFETCH( QString, keyclicks );
    QFETCH( QDate, finalDate );
 
    QDateEdit dateEdit( initialDate );
    // this next line is just to start editing
    QTest::keyClick( &dateEdit, Qt::Key_Enter );
    QTest::keyClicks( &dateEdit, keyclicks );
    QCOMPARE( dateEdit.date(), finalDate );
}
 
QTEST_MAIN(testDateEdit)
#include "tutorial3.moc"

Большая часть кода аналогична коду из предыдущего примера, обратим внимание на новые элементы и наиболее важные изменения по строкам. Строки с 1 по 3 импортируют различные объявления Qt, как и раньше. Строка 4 — макрос, который требуется для управляемого данными тестировании, к нему мы вернемся позже. В строках с 5 по 12 объявляется тестовый класс — некоторые наименование изменены, но он аналогичен предыдущему примеру. Отметим, что функции testValidator и testValidator_data — мы будем снова использовать в управляемом данными тестировании.

Наш первый реальный тест начинается на строке 13. Строка 16 создает QDate, 17 используюет эту дату как начальное значение для виджета QDateEdit. Строки 19 и 20 показывают как мы можем тестировать в случае, если мы нажимаем клавишу «вверх». Функция QTest::keyClick принимает на вход указатель на виджет и символическое имя клавиши (символ или Qt::ShiftModifier для клавиши shift), а также опционально время задержки (в миллисекундах). Как альтернативу использованию QTest::keyClick вы можете использовать QTest::keyPress и QTest::keyRelease для конструирования более комплексных клавиатурных последовательностей. Строки с 23 по 29 показывают аналогичный предудущему тест, но в этом случае мы имитируем щелчок мыши. Нам надо кликнуть в ниженей правой части виджета (щелкнуть на уменьшающей стрелке - см. рис. 1), и это требует знания размера виджета. Строки 23 и 24 вычисляют точку щелчка исходя из размеров виджета. Строка 25 (и такая же 27) имитирует щелчок левой кнопки мыши на вычисленной точке. Аргументы Qt::mouseClick следующие:

  • указатель на виджет по которому происходит щелчок.
  • кнопка мыши, которой щелкают.
  • опционально клавиатурный модификатор (Qt::ShiftModifier) или 0 если без модификаторов.
  • опционально точка щелчка — по умолчанию середина виджета
  • опционально время задержки

В дополнение к QTest::mouseClick, также имеются QTest::mousePress, QTest::mouseRelease, QTest::mouseDClick (двойной щелчок) и QTest::mouseMove. Первые три используются также как QTest::mouseClick. Последняя принимает на вход точку куда переместить мышь. Вы можете комбинировать эти функции для имитации перетаскивания мышью. В строке 35 показывается другой подход к клавиатурному вводу, используя QTest::keyClicks. Где QTest::keyClick посылает одно нажатие клавиши, QTest::keyClicks принимает на вход QString (или его эквивалент, в строке 30 — массив знаков), что представляет последовательность нажатий клавиш. Остальные аргументы такие же. Строки с 35 по 40 показывают как вы можете использовать комбинации функций. После того как вы ввели новую дату в строке 35 курсор находится в конце виджета. В строке 38мы используем Shift+Tab комбинацию для перемещение курсора обратно к месяцу. Затем в строке 39 мы вводим новое значение месяца. Конечно, мы используем отдельные вызовы QTest::keyClick, так как сделать это в одну строку не получится.

Тестирование интерфейса ведомое данными

Строки с 50 по 60 демонстрируют тестирование ведомое данными — в данном случае мы проверяем работу валидатора. Этот как тот случай, когда тестирование ведомое данными может помочь убедиться, что код работает так как он должен. В строках с 51 по 53, мы вписываем начальное значение, последовательность нажатия клавиш, и ожидаемое значение. Эти колонки установлены в строках 47-49. Отметим, что сейчас мы тестируем через QDate, а в предыдущем примере мы использовали три целых числа и получали QDate из них. QDate — не регистрированный тип для QMetaType и поэтому мы должны зарегистрировать его до того как будем использовать его в нашем тестировании. Это делается путём использования макрокоманды Q_DECLARE_METATYPE в строке 4 и функции qRegisterMetaType в строке 38. В строках с 51 по 57 добавляется пара тестирующих строк. В строках с 51 по 53 указан случай, когда ввод верен, а в 55-57 когда только частично верен (день). В настоящем тесте явно должно быть больше комбинаций чем в этом. Эти ряды тестируются в строках 61-72. Мы создаем виджет QDateTime в строке 67 используя инициализирующее значение. Затем посылаем Enter в строке 69, которое переводит виджет в режим редактирования. В строке 70 мы имитируем ввод данных и в строке 71 сравниваем результат с ожидаемым. Строки 74 и 75 такие же, как и в предыдущих примерах.

Повторное использование тестирующих событий.

Если вы постоянно используете один набор событий, то, возможно, будет удобней создать список событий, а потом просто проигрывать его. Это может увеличить гибкость и ясность набора тестов, особенно для движений мышью. Класс для создания списков тестирующих событий известен как QTestEventList. Это QList для QTestEvents. Обычный подход – создать список, а затем использовать различные функции для добавления событий клавиатуры или мыши. Функции, которые вам понадобятся – это addKeyClick и addMouseClick, которые очень похожи на ранее используемые QTest::keyClick и QTest::mouseClick. Для более комплексных операций вы можете использовать addKeyPress, addKeyRelease, addKeyEvent, addMousePress, addMouseRelease, addMouseDClick и addMouseMove. Также вы можете использовать addDelay для добавления задержек между событиями. Когда список составлен, вы можете просто вызывать его для каждого виджета. Вы можете увидеть как это работает в примере ниже, который является примером QDateEdit конвертированного для использования QTestEventList. Пример 14. Тестовый код QDateEdit, используя QTestEventList

#include <QtTest>
#include <QtCore>
#include <QtGui>
Q_DECLARE_METATYPE(QDate)
 
class testDateEdit: public QObject
{
    Q_OBJECT
private slots:
    void testChanges();
    void testValidator_data();
    void testValidator();
};
 
void testDateEdit::testChanges()
{
    // 11 March 1967
    QDate date( 1967, 3, 11 );
    QDateEdit dateEdit( date );
 
    // клавиша «вверх» должна увеличить день на единицу
    QTest::keyClick( &dateEdit, Qt::Key_Up );
    QCOMPARE( dateEdit.date(), date.addDays(1) );
 
    // щелкаем дважды на стрелке вниз в нижнем правом углу
    // сначала надо узнать размер виджета, чтоб знать куда щелкнуть
    QSize editWidgetSize = dateEdit.size();
    QPoint clickPoint(editWidgetSize.rwidth()-2, editWidgetSize.rheight()-2);
    // создаем список из двух щелчков
    QTestEventList list1;
    list1.addMouseClick( Qt::LeftButton, 0, clickPoint);
    list1.addMouseClick( Qt::LeftButton, 0, clickPoint);
    // вызываем список для виджета
    list1.simulate( &dateEdit );
    // день должен уменьшится на два (на единицу меньше, чем сейчас)
    QCOMPARE( dateEdit.date(), date.addDays(-1) );
 
    QTest::keyClicks( &dateEdit, "25122005" );
    QCOMPARE( dateEdit.date(), QDate( 2005, 12, 25 ) );
 
    QTestEventList list2;
    list2.addKeyClick( Qt::Key_Tab, Qt::ShiftModifier );
    list2.addKeyClicks( "08" );
    list2.simulate( &dateEdit );
    QCOMPARE( dateEdit.date(), QDate( 2005, 8, 25 ) );
}
 
void testDateEdit::testValidator_data()
{
    qRegisterMetaType<QDate>("QDate");
 
    QTest::addColumn<QDate>( "initialDate" );
    QTest::addColumn<QTestEventList>( "events" );
    QTest::addColumn<QDate>( "finalDate" );
 
    QTestEventList eventsList1;
    // Key_enter – чтобы начать редактирование 
    eventsList1.addKeyClick( Qt::Key_Enter );
    eventsList1.addKeyClicks( "12041968" );
 
    QTest::newRow( "1968/4/12" ) << QDate( 1967, 3, 11 )
                                 << eventsList1
                                 << QDate( 1968, 4, 12 );
 
    QTestEventList eventsList2;
    eventsList2.addKeyClick( Qt::Key_Enter );
    eventsList2.addKeyClicks( "140abcdef[" );
 
    QTest::newRow( "1967/3/14" ) << QDate( 1967, 3, 11 )
                                 << eventsList2
                                 << QDate( 1967, 3, 14 );
    // сюда можно добавить еще ряды тестовых данных
}
 
void testDateEdit::testValidator()
{
    QFETCH( QDate, initialDate );
    QFETCH( QTestEventList, events );
    QFETCH( QDate, finalDate );
 
    QDateEdit dateEdit( initialDate );
 
    events.simulate( &dateEdit);
 
    QCOMPARE( dateEdit.date(), finalDate );
}
 
QTEST_MAIN(testDateEdit)
#include "tutorial3a.moc"

Этот пример соответствует его предыдущей версии до строки 28. В строке 30 мы создаем QTestEventList. Добавляем события в строках 31, 32 – отметим, что мы не указываем виджет. В строке 34 мы имитируем каждое события для виджета. Еслм у нас несколько виджетов, мы можем имитировать вызовы используя тот же набор событий. Строки с 36 по 39 аналогичны предыдущему примеру. Мы создаем другой список в строках 41-43, теперь мы используем addKeyClick и addKeyClicks добавления событий мыши. Список событий может содержать комбинации событий от мыши и клавиатуры. Мы используем второй список в строке 44 и проверяем результат в строке 45. Вы также можете построить список событий в управляемом данными тестировании, что показано в строках с 50 с 71. Основная разница в том, что вместо добавления QString в каждый набор, мы добавляем QTestEventList. В строках 56-59 мы создаем список событий. В строке 61 добавляем эти события в набор. Вы создаем второй список в строках 65-67 и добавляем второй список в набор в строке 69. Мы извлекаем события в строке 78, а затем используем их в 83-й. Если у нас несколько виджетов, то мы можем использовать тот же список несколько раз.

Руководство 4 - Тестирование при неудачных тестах

При некоторых условиях, невозможно избежать провала тестов. В этом руководстве мы увидим, как поступать в таких случаях.

Пропуск тестов.

Когда тест невозможно выполнить успешно (например, если не хватает нужных файлов или характеристика зависит от особенности архитектуры или операционной системы), наилучшим решением будет пропуск теста. Тест пропускают используя макрос QSKIP. QSKIP имеет два аргумента – строку подписи, которая может использоваться для описания почему тест пропускается и константу из перечисления, которая управляет количеством пропусков теста. Если вы укажете SkipAll в управляемом данными тестировании, то только текущий набор будет пропущен. Если укажете SkipAll, то все последующие наборы будут пропущены. Если тест не управляется данными, то не имеет значения что вы там укажете. Вы можете посмотреть работу QSKIP в примере ниже: Пример 15. Тестирование, показывающее пропуск тестов

void testDate::testSkip_data()
{
    QTest::addColumn<int>("val1");
    QTest::addColumn<int>("val2");
 
    QTest::newRow("1") << 1 << 1;
    QTest::newRow("2") << 1 << 2;
    QTest::newRow("3") << 3 << 3;
    QTest::newRow("5") << 5 << 5;
    QTest::newRow("4") << 4 << 5;
}
 
void testDate::testSkip()
{
    QFETCH(int, val1);
    QFETCH(int, val2);
 
    if ( val2 == 2 ) 
        QSKIP("Two isn't good, not doing it", SkipSingle);
    if ( val1 == 5 )
        QSKIP("Five! I've had enough, bailing here", SkipAll);
    QCOMPARE( val1, val2 );
}

Пример 16. Результат выполнения с пропуском тестов

$ ./tutorial4 testSkip -v2
********* Start testing of testDate *********
Config: Using QTest library 4.1.0, Qt 4.1.0-snapshot-20051107
INFO   : testDate::initTestCase() entering
PASS   : testDate::initTestCase()
INFO   : testDate::testSkip() entering
INFO   : testDate::testSkip(1) COMPARE()
   Loc: [tutorial4.cpp(82)]
SKIP   : testDate::testSkip(2) Two isn't good, not doing it
   Loc: [tutorial4.cpp(79)]
INFO   : testDate::testSkip(3) COMPARE()
   Loc: [tutorial4.cpp(82)]
SKIP   : testDate::testSkip(5) Five! I've had enough, bailing here
   Loc: [tutorial4.cpp(81)]
PASS   : testDate::testSkip()
INFO   : testDate::cleanupTestCase() entering
PASS   : testDate::cleanupTestCase()
Totals: 3 passed, 0 failed, 2 skipped
********* Finished testing of testDate *********

из детализированных результатов вы можете увидеть, что тест был выполнен для первого и третьего наборов. Второй набор не был запущен, так как QSKIP был вызван с SkipSingle. Аналогично, четвертый и пятый наборы не были запущены потому что на четвертом вызвали QSKIP c параметром SkipAll. Также отметим, что тест не был провален, несмотря на два вызова QSKIP. В принципе, пропущенный тест – это тест который не запустился по каким-то уважительным причинам, в отличии от верного теста, но который провалился из-за бага в тестируемом коде.