Функціональне тестування програм на Qt

Передмова

Єдиний спосіб перевірити, що після вашого останнього виправлення, внесеного в систему контролю версій, важливі сценарії використання програми все ще правильно працюють (ну або хоч як-небудь працюють) — це, звичайно ж, взяти і прогнати ці сценарії через систему тестів. Робити це вручну — довго, нудно і чревате помилками.
Враховуючи все вищезазначене, а також той "незначний" факт, що замовник в ТЗ прописав необхідність автоматичного тестування вказаних в тому ж ТЗ функціональних вимог, при старті чергового проекту стало актуальним питання вибору інструменту для автоматизації тестування GUI. Проект був на Qt, потрібна була кросплатформеність (Windows, Linux).
Який в підсумку opensource інструмент з'явився, дивіться за катом.
image

Інструменти для тестування GUI

Які готові рішення для тестування GUI були доступні на той час (кілька років тому)?
Якщо узагальнити, то було два класи можливих утиліт:
  1. Програми, створені спочатку для автоматизації дій користувача, а не для тестування.
    Думаю, кожен може назвати для своєї коханої ОС парочку. Наприклад, для Linux/X11 — див. пост на хабре.
    Ні одна з таких утиліт нам не підійшла, оскільки не задовольняла як мінімум одному з сформульованих вимог:
    1. Кросплатформеність.
      Більшість з них не кроссплатформенны, тобто працюють або тільки на Windows, або тільки на Linux.
    2. Надмірна прив'язка до деталей реалізації.
      Навіть якщо вирішити проблему з кроссплатформенностью (наприклад, запустивши програму на Linux машині, а X server для Windows), то надмірна прив'язка до деталей реалізації призводить до проблем такого характеру.
    найпростіші утиліти записували і відтворювали натискання мишки в системі координат (СК) екрана (тобто інша роздільність екрану — і тест падає), розумніший використовували СК вікна (інший Qt стиль, інший DPI — і тест падає). Найкращі вміли розпізнавати «нативні» віджети ОС, але, на жаль, Qt дуже мало використовував «нативні» елементи конкретної ОС(Qt маскує вигляд своїх віджетів під конкретну ОС, в даному випадку Linux або Windows, але всередині це все той же QWidget).
    Підсумок: ні одна утиліта з цього класу нам не підійшла. Потрібно було б створювати по два варіанти тестів для одного і того ж сценарію роботи користувача — для Linux і Windows. Плюс, дуже складно було б підтримувати створені з допомогою них тести (будь-яка зміна в інтерфейсі їх ламає).
  2. Програми, створені саме для тестування. Враховуючи досвід (1), з цього класу утиліт ми розглядали тільки утиліти, що мають підтримку Qt. Така була рівно одна — squish (опис на хабре c відповідним підходом до ціноутворення — «зв'яжіться з нами, ми оцінимо вас і виставимо ціну». Так було кілька років тому, можливо, зараз щось змінилося. Я їм послав запит, але відповіді так і не дочекався.
Результат закономірний — було прийнято рішення зробити такий інструмент самостійно.

Qt Monkey

Альфа версія
Спочатку завдання здавалася досить простій. Є підсистема Qt з характерною назвою QTest (я її вже використав у проекті для написання модульних тестів для власних віджетів). З допомогою неї досить легко записати послідовність натиснень клавіш і кліків мишки (
QTest::mouseClick
,
QTest::keyCick
). Генерувати код тесту можна за допомогою перетворення
QEvent
->
QTest::something
, попередньо попросивши Qt з допомогою
qApp->installEventFilter
повідомляти про всі події в тестованому додатку. В результаті, попередній варіант був готовий швидко.
Правда, завантаження тестів з допомогою механізму плагінів і написання самих тестів на C++ чомусь не викликала розуміння у QA інженерів. На щастя, в Qt є простий спосіб вбудовування JavaScript в додаток — QtScript. Ця підсистема дозволяє дуже легко, практично парою рядків коду, забезпечити взаємодію спадкоємців
QObject
і JavaScript, транслюючи дзвінки в обидві сторони:
QScriptEngine engine;
QScriptValue global = engine.globalObject();
QScriptValue val = engine.newQObject(qobject);
global.setProperty(QLatin1String("myobject"), val);

а в javascript:
myobject.slot1();
var v = myobject.property1;

Залишилося тільки придумати, як ідентифікувати віджети, адже створити об'єкт (
QObject
), властивості (
Q_PROPERTY
) якого — покажчики на всі коли-небудь створені графічні елементи програми, досить важко.
Після деякого періоду роздумів зупинилися на такій схемі іменування віджета:
«ідентифікатор батьків (якщо є)» точка «ідентифікатор конкретного об'єкта»
де «ідентифікатор батьків (якщо є)» знову розбивається на пару
«ідентифікатор батька батька (якщо є)» точка «ідентифікатор конкретного батька». І так поки
QObject::parent
повертає
nullptr
.
Ідентифікатор конкретного об'єкта може бути або іменем об'єкта — найпростіший випадок (у разі використання Qt Designer ім'я об'єкта буде присутній), якщо ж об'єкт безіменний, то ідентифікуємо його через ім'я класу і порядковий номер.
Приклад:
MainWindow.centralwidget.tabWidget.qt_tabwidget_stackedwidget.tab.pushButton_ModalDialog
Бета версія
Здавалося б, все чудово, тести легко створюються, працюють на обох платформах без будь-яких змін (оскільки не прив'язані до попиксельному розташуванню елементів). Але виявилося, що все не так просто. В дію вступили модальні діалоги (тоді це були діалоги відкриття файлів, але проблему викликав би і банальний
QMessageBox
з Yes/No).
Помилка була в наступному:
  • QTest::что_то_там
    створював потрібний екземпляр спадкоємця
    QEvent
    , і за допомогою
    QApplication::notify
    доставляв його потрібного об'єкту;
  • QApplication::notify
    працює синхронно, тобто поки подія не обробиться, управління назад він не поверне;
  • у випадку з діалогом створюється новий
    QEventLoop
    , і він починає обробляти події, поки діалог не закриють.
Таким чином, управління з
QTest::то_там
не повернеться, поки діалог не закриється. Але як скрипт його може закрити, якщо управління до нього не повернеться, поки діалог не закритий?
Ситуацію ускладнювало ще й те, що блокують поведінка
QTest::то_там
— як раз те, що нам потрібно, виключаючи, звичайно ж, виклик діалогів. Вставляти в тест різного роду перевірки набагато простіше, якщо знаєш, що саме після рядка
Test.activateItem('MainWindow.centralwidget.tabWidget.qt_tabwidget_tabbar', 'Tab 5');
буде активована вкладка
'Tab 5'
, а не через, скажімо, п'ять рядків після неї.
Звичайно, якщо у нас є блокуючий виклик, якого ми не можемо уникнути, очевидним рішенням є створення ще одного потоку, але, на жаль, події, пов'язані з GUI, повинні оброблятися тільки в головному потоці. Тому перший варіант рішення був такий (псевдокод):
//addition thread
qApp->postEvent(objectInGuiThread, customEventObject);
semaphore->tryAcquire(timeout);

//gui thread
void ClassInGuiThread::customEvent()
{
QTest::somthing();
semaphore->release();
}

тобто з допомогою
QEvent
ми повідомляємо об'єкт, що знаходиться в головному (GUI) потоці, про те, що потрібно про щось зробити, а тому що
objectInGuiThread
знаходиться в GUI потоці, то його метод
::customEvent
буде вызыван в контексті GUI потоку, ну і з допомогою семафора здійснюється синхронізація потоків.
Власне, подібним чином і працює механізм signal/slots при виклику
QObject::connect
з параметром
Qt::QueuedConnection
, в тому випадку, коли сигнал надсилається з одного потоку, а об'єкт, якому належить слот, знаходиться в іншому потоці.

Очевидний недолік — потрібно правильно підібрати таймаут у виклику
semaphore->tryAcquire(timeout);
. Якщо, наприклад, натискання на віджет викличе довгу операцію, а ми отвалимся по таймауту, а після цього продовжимо роботу, скажімо, з спроби натискання на віджет, з'являється тільки після завершення довгої операції, то результат може виявитися несподіваним для автора тесту.
Печаль викликає також той факт, що
QCoreApplication::loopLevel()
при переході з Qt 3 з Qt 4 зробили застарілим, і він доступний тільки при складанні Qt 4.x з опцією qt3support, а повернули назад його аналог
QThread::loopLevel()
тільки в Qt 5.5. Тобто складно відрізнити випадок "натискання -> довга операція" від випадку "натискання -> модальний діалог".
Неочевидний недолік цього коду — те, як можуть бути оброблені два поспіль йдуть події, перше з яких закриває модальне вікно, а друге, наприклад, імітує натискання на клавіатурі. У цьому випадку, з-за того, що закриття
QEventLoop
, створеного всередині
QDialog::exec
не миттєво (принаймні, в реалізації
QAbstractEventDispatcher
з допомогою glib,
QKeyEvent
може потрапити в
QEventLoop
класу
QDialog
, і тоді воно не викличе, наприклад, спрацьовування відповідного
QAction
в головному вікні.
Тому остаточний варіант для боротьби з модальними діалогами вийшов досить складний:
  1. Отримати поточний модальний віджет
    qApp->activeModalWidget()
    , причому треба врахувати, що цей метод не позначений як thread-safe, тому код викликається в іншому потоці;
  2. Надіслати повідомлення з проханням виконати потрібну натискання/клік в GUI потоці;
  3. Переконатися з допомогою
    qApp->sendPostedEvents
    , що повідомлення із 2) дійшло
    і початок оброблятися;
  4. Повторити 1) і порівняти результати;
  5. Якщо новий model widget не з'явився, то чекаємо завершення 2) без таймауту, в іншому випадку тільки заданий користувачем таймаут (за замовчуванням 5 секунд).
У цьому алгоритмі теж є проблеми (якщо новий діалог "з'являється" або "зникає" більше 5 секунд), але в цьому випадку, використовуючи JavaScript, можна виставити таймаут побільше, або ще як-то синхронізуватися. Також не обробляється варіант використання користувачем
QEventLoop
без створення модального діалогу. Після перемоги над діалогами qt monkey заробив досить стабільно, і проект був зданий.
Версія на github
оскільки рішень з відкритим вихідним кодом для подібного роду завдань досі я не бачив, то, перебуваючи у вимушеній відпустці, я вирішив випустити проект у вільне плавання. Хоча попередній работатадель не заперечував проти публікації qt monkey, я, на всяк випадок переписавши з його нуля, виклав на github.
Поточна версія складається з трьох компонентів:
  1. Бібліотека, яку потрібно слинковать з вашим проектом, після чого створити клас
    qt_monkey_agent::Agent
    де-небудь в головному потоці;
  2. qtmonkey_app — консольна програма для спілкування за 1), за допомогою неї, наприклад, можна запускати ваші тести в системі continuous integration;
  3. qtmonkey_gui — елементарний GUI для qtmonkey_app.
2 і 3 спілкуються за допомогою
stdout
/
stdin
, потоки даних структуровані за допомогою JSON. Передбачається, що qtmonkey_gui можна легко замінити плагіном для вашого улюбленого IDE.
Якщо знайдуться люди, яким буде цікавий цей проект, то його легко знайти за словами "qt monkey" на github'е. Pull requests вітаються.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.