Я делаю разный софт на Go, Python и C++
0,0
рейтинг
25 июня 2012 в 10:33

Разработка → Юнит-тестирование в Qt tutorial


gollum подметил что в тексте картинки есть ошибка

Салют, хабр! Как дела?

Хотел немного подучится чему-то. Искал на хабре в хабе «Qt Software» хоть какой-то пост про юнит-тестирование в Qt. Не нашел. Тут я расскажу базовые вещи про юнит-тестирование на Qt (не ожидайте могучего шаманства). На самом деле, юнит-тестить в Qt довольно просто. Что бы узнать как это делать, приглашаю читать дальше.

Я постараюсь разбить все на части. Погрупирую, так сказать. Начнем.

Теория


Если вы знаете теорию юнит-тестирования — можете пропустить этот пункт.

У нас есть код. Как бы мы этого не хотели в нем есть баги. Баги — это плохо. Чтобы багов не было, нужно писать очень качественный код (это не в этой статье) и главное, тестировать его. Но мы пишем код, дополняем, рефакторим… И каждый раз поддавать каждую версию проекта одному и тому же набору тестов — неприятно. И тут один очень мудрый программист додумался сделать такую программу, которая могла бы тестировать вашу программу этим, заветным набором тестов. И эта модель тестирования называется — юнит-тестинг!

Unit-testing в Qt


А теперь конкретнее. В Qt за юнит-тестирование отвечает модуль QTestLib (testlib). Он предоставляет нам набор макросов для тестирования. Но об этом позже. Есть несколько методов проведения тестов:
  • Завести тестовый проект в дочерней директории вашего проекта и тестировать в нем.
  • Тестировать макросом qExec(..) в основном проекте


Я чаще использую первый метод, второй — уродлив. Но сегодня я вам покажу на примере второго метода, а первый метод распишу сейчас.

Qt использует прикольную модель: один проект — один тест. Поэтому реализовываются тесты созданием проекта tests в дочерней директории tests основного проекта. В tests лежит класс реализующий тест основного класса. Принцип работы его вы узнаете позже, а основное отличие этого подхода лежит в способе запуска теста. Этот подход требует отсутствие main.cpp и наличие макроса Q_TEST_MAIN(Test_ClassName) в конце test_classname.cpp.

Задача


Предлагаю для примера, реализовать класс Smart, который будет работать с сравнением целых чисел. Что он конкретно будет делать? Реализуем метод int min(int, int), который будет возвращать меньшее число и int max(int, int), который вернет большее число.

Ну давайте уже!


Так. Заходим в Qt Creator. Создаем консольное приложение Qt. Добавляем модуль testlib и gui (надо для тестирования GUI) к .pro-файлу. Теперь можно начинать. Принято начинать с написания тестов, а потом уже самого класса, но я пожалуй отклонюсь от традиций. Будем писать класс Smart. Вам повезло, напишу его я. Вам надо только понять как он работает. Вот этот красавец:

smart.h
#ifndef SMART_H
#define SMART_H

#include <QObject>
#include <QStringList>

class Smart : public QObject
{
    Q_OBJECT
public:
    explicit Smart(QObject *parent, const QStringList& list);
    
public slots:
    int max(int a, int b);
    int min(int a, int b);

};

#endif // SMART_H


smart.cpp
#include "smart.h"

Smart::Smart(QObject *parent, const QStringList& list) :
    QObject(parent)
{
}

int Smart::max(int a, int b)
{
    if(a > b)
        return a;
    return b;
}

int Smart::min(int a, int b)
{
    if(a < b)
        return a;
    return b;
}


Тестирование QObject* классов


Класс готов. Самое время проверить как он работает! Для этого напишем класс который будет тестировать наш «умный» класс. Он будет называется Test_Smart.

test_smart.h
#ifndef TEST_SMART_H
#define TEST_SMART_H

#include <QObject>

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

private slots: // должны быть приватными
    void max(); // int max(int, int)
    
};

#endif // TEST_SMART_H


test_smart.cpp
#include <QTest>
#include "test_smart.h"
#include "smart.h"

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

void Test_Smart::max()
{
    Smart a;
    QCOMPARE(a.max(1,   0), 1);
    QCOMPARE(a.max(-1,  1), 1);
    QCOMPARE(a.max(4,   8), 8);
    QCOMPARE(a.max(0,   0), 0);
    QCOMPARE(a.max(1,   1), 1);
    QCOMPARE(a.max(-10,-5), -5);
}


Мы немного не дописали, но это не страшно. Еще успеем. Сейчас надо научится запускать наши тесты.

main.cpp
#include <QApplication>
#include <QTest>
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include "test_smart.h"

using namespace std;

int main(int argc, char *argv[])
{
    freopen("testing.log", "w", stdout);
    QApplication a(argc, argv);
    QTest::qExec(new Test_Smart, argc, argv);
    return 0;
}


Компилируем…

testing.log
********* Start testing of Test_Smart *********
Config: Using QTest library 4.8.1, Qt 4.8.1
PASS   : Test_Smart::initTestCase()
PASS   : Test_Smart::max()
PASS   : Test_Smart::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of Test_Smart *********


Поверьте, это — самый лучший исход тестирования!

Но мы еще не протестировали один метод. Я его оставил, так как хочу показать на нем один прием тестирования. Я называю его просто — "табличка". Суть этого метода в том, чтобы не повторять код. Помните наш тестовый метод void max()? Там мы много раз повторяли один и тот же самый код (разве что с разными параметрами). Чтобы этого избежать, в Qt реализован метод — «табличка». А как он работает? Создаем метод method_data(), в нем проводим пару нехитрых операций, а потом загружаем все это макросом QFETCH(). Сейчас как раз время увидеть это все на практике!

Теперь пора добавить в test_smart.cpp реализацию нашей «таблички»:

void Test_Smart::min_data()
{
    QTest::addColumn<int>("first");
    QTest::addColumn<int>("second");
    QTest::addColumn<int>("result");

    QTest::newRow("min_data_1") << 1 << 0 << 0;
    QTest::newRow("min_data_2") << -1 << 1 << -1;
    QTest::newRow("min_data_3") << 4 << 8 << 4;
    QTest::newRow("min_data_4") << 0 << 0 << 0;
    QTest::newRow("min_data_5") << 1 << 1 << 1;
    QTest::newRow("min_data_6") << -10 << -5 << -10;
}

void Test_Smart::min()
{
    Smart a;
    QFETCH(int, first);
    QFETCH(int, second);
    QFETCH(int, result);
    QCOMPARE(a.min(first, second), result);
}


Теперь опять компилируем. Получаем вывод.

testing.log
********* Start testing of Test_Smart *********
Config: Using QTest library 4.8.1, Qt 4.8.1
PASS   : Test_Smart::initTestCase()
PASS   : Test_Smart::max()
PASS   : Test_Smart::min()
PASS   : Test_Smart::cleanupTestCase()
Totals: 4 passed, 0 failed, 0 skipped
********* Finished testing of Test_Smart *********


Теперь где-нибудь что-то неправильно сделаем. Например поменяем в Smart::min(..) поменяем < на >.
testing.log
********* Start testing of Test_Smart *********
Config: Using QTest library 4.8.1, Qt 4.8.1
PASS   : Test_Smart::initTestCase()
PASS   : Test_Smart::max()
FAIL!  : Test_Smart::min(data_1) Compared values are not the same
   Actual (a.min(first, second)): 1
   Expected (result): 0
   Loc: [test_smart.cpp(41)]
FAIL!  : Test_Smart::min(data_1) Compared values are not the same
   Actual (a.min(first, second)): 1
   Expected (result): -1
   Loc: [test_smart.cpp(41)]
FAIL!  : Test_Smart::min(data_1) Compared values are not the same
   Actual (a.min(first, second)): 8
   Expected (result): 4
   Loc: [test_smart.cpp(41)]
FAIL!  : Test_Smart::min(data_1) Compared values are not the same
   Actual (a.min(first, second)): -5
   Expected (result): -10
   Loc: [test_smart.cpp(41)]
PASS   : Test_Smart::cleanupTestCase()
Totals: 3 passed, 4 failed, 0 skipped
********* Finished testing of Test_Smart *********


Значит все хорошо).

Тестируем GUI


Иногда, а иногда даже очень часто, нам приходится тестировать графический интерфейс. В QTestLib это тоже реализовано. Давайте протестируем QLineEdit.

Вот как выглядит наш test_qlineedit.h:
#ifndef TEST_QLINEEDIT_H
#define TEST_QLINEEDIT_H

#include <QObject>

class Test_QLineEdit : public QObject
{
    Q_OBJECT
private slots: // должны быть приватными
    void edit();
    
};

#endif // TEST_QLINEEDIT_H


А вот как выглядит, тоже наш test_qlineedit.cpp:
#include <QtTest>
#include <QtGui>
#include "test_qlineedit.h"

void Test_QLineEdit::edit()
{
    QLineEdit a;
    QTest::keyClicks(&a, "abCDEf123-");

    QCOMPARE(a.text(), QString("abCDEf123-"));
    QVERIFY(a.isModified());
}


Пора поправить main.cpp:
#include <QApplication>
#include <QTest>
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include "test_smart.h"
#include "test_qlineedit.h"

using namespace std;

int main(int argc, char *argv[])
{
    freopen("testing.log", "w", stdout);
    QApplication a(argc, argv);
    QTest::qExec(new Test_Smart, argc, argv);
    cout << endl;
    QTest::qExec(new Test_QLineEdit, argc, argv);
    return 0;
}


Теперь запускаем тестирование:
********* Start testing of Test_Smart *********
Config: Using QTest library 4.8.1, Qt 4.8.1
PASS   : Test_Smart::initTestCase()
PASS   : Test_Smart::max()
PASS   : Test_Smart::min()
PASS   : Test_Smart::cleanupTestCase()
Totals: 4 passed, 0 failed, 0 skipped
********* Finished testing of Test_Smart *********

********* Start testing of Test_QLineEdit *********
Config: Using QTest library 4.8.1, Qt 4.8.1
PASS   : Test_QLineEdit::initTestCase()
PASS   : Test_QLineEdit::edit()
PASS   : Test_QLineEdit::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of Test_QLineEdit *********


Вот мы и научились тестировать GUI. Тест показал что QLineEdit работает корректно)).

Аргументы тестирования


Опция Объяснение
-o filename Выведет результаты тестирования в файл filename
-silent Ограничить сообщения показом только предупреждений и ошибок
-v1 Отображать информацию о входе и и выходе тестовых методов
-v2 Дополняет опцию -v1 тем, что выводит сообщения для макросов QCOMPARE и QVERIFY
-vs Отображать каждый высланный сигнал и вызванный слот
-xml Осуществлять вывод всей информации в формате XML
-eventdelay ms Заставляем тест остановиться и подождать ms миллисекунд. Эта опция полезна для нахождения ошибок в элементах GUI

Всего того, что я вам сегодня расказал, точно хватит чтобы прямо сейчас начать тестировать свои Qt-приложения. Что я могу вам сказать? Все советы и пожелания для улучшения статьи прошу написать в комментариях — для меня это важно, так как это, надеюсь, не последняя моя статья.

Удачи и хорошого вам кода;)
Илья @namespace
карма
22,7
рейтинг 0,0
Я делаю разный софт на Go, Python и C++
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (20)

  • +1
    Спасибо! Очень актуально. Сам на выходных искал простенький «Getting started».
    Разъясните, пожалуйста, для адептов VS следующее:
    Добавляем модуль testlib и gui (надо для тестирования GUI) к .pro-файлу.

    Да и про добавление дочернего проекта для тестирования, если несложно.
    • +2
      1. Открываете .pro файл (это файл проекта на qt используемый как qtcreator, так и собственно qmake) и дописываете в него директиву QT:
      QT += testlib gui

      таким образом вы подключаете необходимые модули.

      2. Qt поддерживает иерархию проектов для этого в корневом .pro-файле следует указать директиву SUBDIRS и перечислить все подпроекты, которые должны принадлежать корневому проекту, естественно в каждом подпроекте должен быть свой .pro-файл. Когда вы дадите команду на сборку корневого проекта все подпроекты будут собраны в указанном порядке.
    • +1
      1. Создаете основной проект.
      2. В папке основного проекта создаете проект tests, там появится папка tests с самим тестовым проектом.
      3. Работаете в тестовом проекте.
      4. Когда все готово, добавляете в основной .pro-файл строчку: SUBDIRS += tests.
      5. Теперь при сборке проекта, будут собираться тесты.
    • +2
      Для адептов VS QtTest(d)4.lib и QtGui(d)4.lib в Input вкладки Linker в Properties
    • +2
      А проект есть проект, создаете новый проект в студии и вешаете на него зависимость от основного функционала. Студия не имеет иерархии проектов
      • 0
        В студии можно устанавливать зависимости между проектами. Тогда зависимый проект будет собираться строго после того, от которого он зависит.
        При генерации sln из subdirs можно сделать так:
        qmake proj.pro -tp vc -r
        тогда qmake рекурсивно пройдет по всем проектам subdirs и осздаст .sln файл + набор .vcproj файлов для каждого субпроекта. При этом порядок сборки будет определен порядком подключения субпроектов в subdirs. Но почему-то в обратном порядке :)

        Делал так, это работает.
  • +4
    спасибо за статью. сравнение двух чисел это, конечно, показательный пример, но хотелось бы более сложных сценариев тестирования. например, асинхронный доступ к веб-сервису, работа с бд, тестирование ui
    • 0
      Тестирование ui описано в последней части поста, может и слабо. Но я думал, принцип понятен. Там все просто, только симуляция ивентов. Например QTest::keyClicks(..). А более сложные примеры точно так же и работают. То, что вы описали: асинхронный доступ к веб-сервису, работа с бд, это все реализуется не в тестовых классах, а в тестируемом классе. В нем может быть что угодно. Тестировать можно все что угодно. Методы Q_COMPARE, Q_VERIFY и т.д.
    • +1
      Для тестирования асинхронных запросов отлично подходит класс QSignalSpy, работа с БД вообще не представляет ничего сложного — отправил запрос, получил ответ, сравнил полученный результат с ожидаемым. Для тестирования Ui именно в понимании Ui т.е. размещения объектов на форме и прочего дизайна существует утилита Squish от froglogic, к сожалению не бесплатная, впрочем, они предоставляют 30-дневный триал.
    • 0
      Асинхронный доступ к веб сервису и работа с БД выходит за рамки UT.
  • +1
    А как же mock-и?
    • +1
      А никак. Нет их тут.
    • 0
      Кстати, Google Testing Framework и Google Mock Framework очень не плохо работают в паре с QTestLib, так что если нужны mock'и то рекомендую посмотреть в эту сторону.
  • +1
    Один вопрос, а как лучше влезать в приватные методы у тестируемых классов?
    • +1
      Только неявным способом, unit тестирование предполагает тестирование public API.
      Кстати, если нужно дотянутся до приватного члена класса, то очень помогает метод QObject::findChild()
      • 0
        findChild же помогает вытянуть всякие классы, которые не живут в паблике. Помню какой-то дефайн позволял в случае чего friend'ом обьявлять тестовый класс.
        И часто бывает, что класс то не совсем самостоятельный, а часть чего-то большого, вот тут приходится или наследника городить или с приватными членами извращаться.
        • 0
          Не помню где, но видел очень остроумное решение: #define private public
          Хотя тестировать приватные члены не правильно.
          • –1
            Подобное решение может быть чревато потенциальными проблемами, но да, я тоже встречал такой подход.
            • +1
              Какие могут быть проблемы с деинкапсуляцией класса в тесте?
  • 0
    Вы просили оставлять любые комментарии по статье, чтобы вы повышали качество их написания.

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

    Стилистически и логически предложения не совсем верные. Я не знаток родной речи, но сами формулировки можно упростить. Написать тоже самое более последовательно.

    Содержание статьи отличное! Желаю дальнейших творческих успехов.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.