понедельник, 26 марта 2012 г.

"Реальное" использование QTestLib

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

Единственный инструмент, который я нашел - QTestLib. К сожалению, его описание весьма скромное:
  1. Официальные мануал и туториал
  2. Небольшой туториал от команды KDE
  3. Автотесты в дереве исходников Qt
К сожалению, официальная документация описывает сферический QString в вакууме, в который поместили "Hello world!". Разумеется в этом случае мне совсем не хотелось бросаться в омут с головой и резать по живому. Поэтому для экспериментов придумал простенькое приложение "Подсчет площади квадрата", о котором и пойдет речь дальше.

Как и полагается, начал я с теста.
#include <QtTest/QtTest>
#include "ui_SqAreaForm.h"

class TestGui: public QObject
{
  Q_OBJECT

private slots:
  void NormalAreaCalc_data()
  {
    QTest::addColumn<QTestEventList>("events");
    QTest::addColumn<QString>("expected");

    QTestEventList list1;
    list1.addKeyClick('0');
    QTest::newRow("zero") << list1 << "0";

    QTestEventList list2;
    list2.addKeyClicks("10");
    QTest::newRow("sq_10") << list2 << "100";
  }

  void NormalAreaCalc()
  {
    QFETCH(QTestEventList, events);
    QFETCH(QString, expected);

    QWidget wdg;
    Ui_SqAreaForm frm;
    frm.setupUi(&wdg);

    events.simulate(frm.side);

    QCOMPARE(frm.area->text(), expected);
  }
};

QTEST_MAIN(TestGui)

#include "test.moc"
Т.е. я планирую создать в дизайнере ui, в котором есть поле ввода side и QLabel area, и ожидаю, что площадь для квадрата с нулевой стороной - ноль, а для 10 - 100. Пока все просто и практически по туториалу. Теперь добавим класс, который будет управлять этим самим ui.
#include <QWidget>

class Ui_SqAreaForm;

class SqAreaForm : public QWidget
{
  Q_OBJECT
  
public:
  SqAreaForm();
  ~SqAreaForm();

#ifdef _TESTING_
  Ui_SqAreaForm* GetUi();
#endif
  
private:
  Ui_SqAreaForm* ui;
};
Помимо типичного кода я добавил еще один метод специально для тестирования. Конечно, неприятно вносить изменения в код ради тестирования, но это изменение минимально и не влияет на работу приложения. Можно обойтись и без добавления этого метода, но тогда придется получать доступ к виджетам через QApplication::allWidgets(), что во много раз сложнее. Существует еще один вариант, когда наша форма предоставляет геттеры и сеттеры, но, как минимум, главному окну такая функциональность точно не нужна.

А теперь самое интересное добавляем слот для реакции на изменение поля side:
void SqAreaForm::OnSqSideChanged(const QString& newSide)
{
  int side = newSide.toInt();
  int area = side*side;
  ui->area->setText(QString::number(area));
}
Что ж - тесты пройдены и это не может не радовать. Но что делать с ui, который мы не можем вернуть через GetUi? Например, диалоговые окна или окна с сообщениями. Для этого поменяем требования к нашему приложению - пусть площадь рассчитывается после нажатия кнопки и результат появляется во всплывающем окне. В этом случае протестировать работу приложения будет немного сложнее:
#include <QtTest/QtTest>
#include "ui_SqAreaForm.h"
#include "SqAreaForm.h"
#include <QApplication>
#include <QMessageBox>
#include <QTimer>

class TestGui: public QObject
{
  Q_OBJECT

private slots:
  void initTestCase()
  {
    msgBoxProcessTimer.setInterval(1000);
    msgBoxProcessTimer.setSingleShot(true);
    QVERIFY(QObject::connect(&msgBoxProcessTimer, SIGNAL(timeout()), this, SLOT(MsgBoxProcess())));
  }

  void NormalAreaCalc_data()
  {
    QTest::addColumn<QTestEventList>("events");
    QTest::addColumn<QString>("expected");

    QTestEventList list1;
    list1.addKeyClick('0');
    QTest::newRow("zero") << list1 << "Area of the sqare with side 0 is 0";

    QTestEventList list2;
    list2.addKeyClicks("10");
    QTest::newRow("sq_10") << list2 << "Area of the sqare with side 10 is 100";
  }

  void NormalAreaCalc()
  {
    QFETCH(QTestEventList, events);
    QFETCH(QString, expected);

    SqAreaForm frm;
    Ui_SqAreaForm* ui = frm.GetUi();

    msgBoxProcessTimer.start();
    events.simulate(ui->side);
    QTest::mouseClick(ui->calc, Qt::LeftButton);
    
    QCOMPARE(msgBoxMessage, expected);
  }
  
  void MsgBoxProcess()
  {
    QMessageBox* msgBox = dynamic_cast<QMessageBox*>(QApplication::activeModalWidget());
    if (msgBox != 0)
    {
      msgBoxMessage = msgBox->text();
      msgBox->close();
    }
  }
  
private:
  QTimer msgBoxProcessTimer;
  QString msgBoxMessage;
};

QTEST_MAIN(TestGui)

#include "test.moc"
Идея теста очень проста: т.к. QDialog::exec() блокирует поток выполнения, нам необходимо взвести таймер, чтобы через 1 секунду обработать уже появившееся окно. Решение, на мой взгляд, не самое удачное, т.к. каждый подобный тест занимает не меньше 1 секунды. Можно поступить более мудро и проверять появление модального окна, скажем, каждые 10мс. Для этого меняем интервал таймера и делаем его не одноразовым(setSingleShot(false)).

С помощью двух описанных выше методик можно вполне сносно протестировать типичные сценарии поведения GUI приложений, написанных на Qt.
Исходники примеров тут.

1 комментарий:

  1. Долго пыталась разобраться с тестами GUI. Спасибо, что потрудились поделиться наработками - с "живыми" примерами разобраться намного проще.

    ОтветитьУдалить