Как выяснилось, разработчики впихнули в QT целый фреймворк для автоматического тестирования, который в том числе умеет и эмулировать внешние действия пользователя на интерфейсе. Это называется QtTestLib. Другие разработчики не стали все это добро целиком тащить в PyQt, так как для питона и так предостаточно библиотек тестирования. А вот механизм эмуляции внешних телодвижений пользователя перенесли, спасибо.
Итак PyQt4.QtTest. Что это такое и с чем его едят читаем в документации. А я расскажу, как тыкал это пальцем на конкретном примере.
Для примера возьмем простейшее PyQt-приложение, которое умеет выполнять основные арифметические действия над двумя операндами и предварительно проверять операнды на корректность.
Приложение слеплено с помощью моей любимой обертки, описанной ранее, а в рамках текущего повествования интересен только класс обработки сигналов от формы. Он тривиален и содержит единственный слот, обрабатывающий нажатие кнопки "=".
class MainWndHandler(object): def calculate(self): u'''Обработчик нажатия кнопки btnCalc. Провека корректности параметров и вычисление результата.''' try: p1 = float(self.ui.edParam1.text()) except ValueError, e: self.ui.edResult.setText(u'Invalid parameter #1: {0}'.format(str(e))) return try: p2 = float(self.ui.edParam2.text()) except ValueError, e: self.ui.edResult.setText(u'Invalid parameter #2: {0}'.format(str(e))) return op = self.ui.cbOperation.currentText() if u'+' == op: res = p1 + p2 elif u'-' == op: res = p1 - p2 elif u'*' == op: res = p1 * p2 elif u'/' == op: try: res = p1 / p2 except ZeroDivisionError: self.ui.edResult.setText(u'Division by zero') return self.ui.edResult.setText(str(res))
Можно скачать архив с исходниками.
Стартовый скрипт приложения - main.py, можно запустить и поиграться, посмотреть, как обрабатывается некорректный ввод операндов.
А теперь самое интересное! Пишем тесты для GUI. Я использую unittest, но это непринципиально. Получилось у меня нечто следующее (я вырезал из теста похожие куски, полная версия в архиве):
#coding=utf-8 # служебные классы/модули import sys import unittest from PyQt4.QtGui import QApplication from PyQt4.QtTest import QTest from PyQt4.QtCore import Qt # тестируемые сущности from wfactory import CWidgetFactory from mainwnd import MainWndHandler class TestQtGui(unittest.TestCase): def setUp(self): u'''Подготовка фикстуры''' # Требование QT - перед созданием виджета необходимо создать приложение. # Приложение создается однократно self.app = QApplication.instance() or QApplication(sys.argv) # Создаваемое окно не назначается главным окном приложения, т.к. приложение не будет запускаться self.w = CWidgetFactory.create("MainWindow.ui", [MainWndHandler])() def test_empty_param1(self): QTest.mouseClick(self.w.ui.btnCalc, Qt.LeftButton) self.assertTrue(unicode(self.w.ui.edResult.text()).startswith(u'Invalid parameter #1: ')) def test_bad_param2(self): QTest.keyClicks(self.w.ui.edParam1, u'2') QTest.keyClicks(self.w.ui.edParam2, u'aaa') QTest.mouseClick(self.w.ui.btnCalc, Qt.LeftButton) self.assertTrue(unicode(self.w.ui.edResult.text()).startswith(u'Invalid parameter #2: ')) def test_addition(self): QTest.keyClicks(self.w.ui.edParam1, u'2') QTest.keyClicks(self.w.ui.edParam2, u'8.0') QTest.mouseClick(self.w.ui.cbOperation, Qt.LeftButton) QTest.keyClick(self.w.ui.cbOperation, u'+') QTest.keyClick(self.w.ui.cbOperation, Qt.Key_Enter) QTest.mouseClick(self.w.ui.btnCalc, Qt.LeftButton) self.assertEqual(self.w.ui.edResult.text(), unicode(10.)) # тестирование вычитания, умножения и деления полностью аналогично сложению def test_division_by_zero(self): QTest.keyClicks(self.w.ui.edParam1, u'18') QTest.keyClicks(self.w.ui.edParam2, u'0.0') QTest.mouseClick(self.w.ui.cbOperation, Qt.LeftButton) QTest.keyClick(self.w.ui.cbOperation, u'/') QTest.keyClick(self.w.ui.cbOperation, Qt.Key_Enter) QTest.mouseClick(self.w.ui.btnCalc, Qt.LeftButton) self.assertEqual(self.w.ui.edResult.text(), u'Division by zero') if __name__ == '__main__': suite = unittest.TestLoader().loadTestsFromTestCase(TestQtGui) unittest.TextTestRunner(verbosity=2).run(suite)
Обзовем файл test_ui_tcalc.py, находиться он должен в той же директории, что и остальные исходники, чтобы избежать сложностей с импортом тестируемых сущностей. Описание структуры хранения тестов в проекте выходит за рамки поста. Возможно опишу позже, когда придумаю :).
Запуск теста дает следующий результат:
test_addition (__main__.TestQtGui) ... ok test_bad_param1 (__main__.TestQtGui) ... ok test_bad_param2 (__main__.TestQtGui) ... ok test_division (__main__.TestQtGui) ... ok test_division_by_zero (__main__.TestQtGui) ... ok test_empty_param1 (__main__.TestQtGui) ... ok test_empty_param2 (__main__.TestQtGui) ... ok test_multiplication (__main__.TestQtGui) ... ok test_subtraction (__main__.TestQtGui) ... ok ---------------------------------------------------------------------- Ran 9 tests in 0.645s OK
Удивительно, все работает :)
А теперь давайте "ошибемся" в логике приложения, перепутаем обработку сложения и вычитания:
if u'+' == op: res = p1 - p2 elif u'-' == op: res = p1 + p2
Результат:
test_addition (__main__.TestQtGui) ... FAIL test_bad_param1 (__main__.TestQtGui) ... ok test_bad_param2 (__main__.TestQtGui) ... ok test_division (__main__.TestQtGui) ... ok test_division_by_zero (__main__.TestQtGui) ... ok test_empty_param1 (__main__.TestQtGui) ... ok test_empty_param2 (__main__.TestQtGui) ... ok test_multiplication (__main__.TestQtGui) ... ok test_subtraction (__main__.TestQtGui) ... FAIL ====================================================================== FAIL: test_addition (__main__.TestQtGui) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\Work\Master\projects\python\TestCompl\test_ui_tcalc.py", line 52, in test_addition self.assertEqual(self.w.ui.edResult.text(), unicode(10.)) AssertionError: PyQt4.QtCore.QString(u'-6.0') != u'10.0' ====================================================================== FAIL: test_subtraction (__main__.TestQtGui) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\Work\Master\projects\python\TestCompl\test_ui_tcalc.py", line 61, in test_subtraction self.assertEqual(self.w.ui.edResult.text(), unicode(.5)) AssertionError: PyQt4.QtCore.QString(u'2.5') != u'0.5' ---------------------------------------------------------------------- Ran 9 tests in 0.808s FAILED (failures=2)
Поразительно, что и ожидалось.
А теперь несколько положительных выводов:
- Тесты оперируют преимущественно объектами интерфейса. Если вы заметили, в коде тестов ни разу не встречаются координаты, что является обычной практикой и источником постоянных проблем при использовании внешних cистем автоматизации тестирования GUI. Однако, при крайней необходимости, координаты таки можно задавать;
- Не нужно заморачиваться с интеграцией тестов GUI с остальными тестами. Они ничем не отличаются :);
- Тестирование не поднимает окна, все делается в фоне. Это дает возможность предположить, что такие тесты сможет запускать сервис тестирования. Нужно проверить, еще одна тема для поста.
Правда, есть один маленький и, наверное, непринципиальный минус. Никто не удосужился написать инструмент для записи телодвижений пользователя на интерфейсе в виде готовых тестов :). "А, ладно, и так сойдет".
В настоящий момент плотно изучаю тестирование приложений, написанных на PyQt4. Не могу понять, как можно протестировать модальный диалог. Я могу запустить приложение, настроить его, но затем мне надо заполнить поля модального диалога и нажать на нём кнопку, но это не получается.
ОтветитьУдалитьК сожалению, я так и не реализовал полноценное тестирование UI своего проекта на PyQt, по большей части именно из-за этого. Не получается этим механизмом протестировать взаимодействие нескольких окон, только логику виджетов в пределах окна (что довольно малоценно). Может и можно как-то, но искал поверхностно и не нашел. Если найдете что-нибудь, не сочтите за труд запостить ссылочку.
ОтветитьУдалить