PyQt5桌面应用开发(20):界面设计结果自动测试(一)
PyQt5桌面应用系列
- PyQt5桌面应用开发(1):需求分析
- PyQt5桌面应用开发(2):事件循环
- PyQt5桌面应用开发(3):并行设计
- PyQt5桌面应用开发(4):界面设计
- PyQt5桌面应用开发(5):对话框
- PyQt5桌面应用开发(6):文件对话框
- PyQt5桌面应用开发(7):文本编辑+语法高亮与行号
- PyQt5桌面应用开发(8):从QInputDialog转进到函数参数传递
- PyQt5桌面应用开发(9):经典布局QMainWindow
- PyQt5桌面应用开发(10):界面布局基本支持
- PyQt5桌面应用开发(11):摸鱼也要讲基本法,两个字,16
- PyQt5桌面应用开发(12):QFile与线程安全
- PyQt5桌面应用开发(13):QGraphicsView框架
- PyQt5桌面应用开发(14):数据库+ModelView+QCharts
- PyQt5桌面应用开发(15):界面动画
- PyQt5桌面应用开发(16):定制化控件-QPainter绘图
- PyQt5桌面应用开发(17):类结构+QWebEngineView
- PyQt5桌面应用开发(18):自定义控件界面设计与实现
- PyQt5桌面应用开发(19):事件过滤器
- PyQt5桌面应用开发(20):界面设计结果自动测试(一)
PyQt5的测试驱动开发(Test-Driven Development,TDD)
测试驱动开发(Test-Driven Development,TDD)是一种软件开发过程,它要求在编写功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。测试驱动开发的目标是使代码更加简洁、可读和可维护。
测试驱动开发可以使程序员在编写代码之前思考软件设计。
测试驱动开发的步骤:
- 添加一个测试
- 运行所有测试,新添加的测试应该失败
- 编写实现代码
- 运行所有测试,若有测试失败,重复步骤3-4
- 重构代码
- 重复步骤1-5
- 完成
QTest
QTest是Qt提供的一个单元测试框架,它提供了一些宏,可以用来测试Qt程序的各个部分,包括GUI部分。
大概探索一下pyqt中实现的,QTest,包括几个部分,几个内置的类(大写开头),内置的枚举类(大写开头),内置的函数(小写开头)。
其中函数主要的作用就是模拟UI上的操作,比如键盘动作、鼠标动作、触控动作等。
下面总结了等待休眠函数(q打头)、键盘动作函数(key打头)、鼠标动作函数(mouse打头)、触控动作函数(touch打头)的函数名称和重载的参数。
UI动作函数
-
小写字母q打头的函数,测试UI的控制函数,包括睡眠、等待、等待窗口激活、等待窗口暴露。
- qSleep
- qSleep(ms: int)
- qWait
- qWait(ms: int)
- qWaitForWindowActive
- qWaitForWindowActive(window: QWindow, timeout: int = 5000) -> bool
- qWaitForWindowActive(widget: QWidget, timeout: int = 5000) -> bool
- qWaitForWindowExposed
- qWaitForWindowExposed(window: QWindow, timeout: int = 5000) -> bool
- qWaitForWindowExposed(widget: QWidget, timeout: int = 5000) -> bool
- qSleep
-
key开头的函数
-
keyClick
- keyClick(widget: QWidget, key: Qt.Key, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
Qt.NoModifier,
delay: int = -1) - keyClick(widget: QWidget, key: str, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
Qt.NoModifier, delay:
int = -1) - keyClick(window: QWindow, key: Qt.Key, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
Qt.NoModifier,
delay: int = -1) - keyClick(window: QWindow, key: str, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
Qt.NoModifier, delay:
int = -1)
- keyClick(widget: QWidget, key: Qt.Key, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
-
keyClicks
- keyClicks(widget: QWidget, sequence: str, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
Qt.NoModifier,
delay: int = -1)
- keyClicks(widget: QWidget, sequence: str, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
-
keyEvent
- keyEvent(action: QTest.KeyAction, widget: QWidget, key: Qt.Key, modifier:
Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] = Qt.NoModifier, delay: int = -1) - keyEvent(action: QTest.KeyAction, widget: QWidget, ascii: str, modifier:
Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] = Qt.NoModifier, delay: int = -1) - keyEvent(action: QTest.KeyAction, window: QWindow, key: Qt.Key, modifier:
Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] = Qt.NoModifier, delay: int = -1) - keyEvent(action: QTest.KeyAction, window: QWindow, ascii: str, modifier:
Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] = Qt.NoModifier, delay: int = -1)
- keyEvent(action: QTest.KeyAction, widget: QWidget, key: Qt.Key, modifier:
-
keyPress
- keyPress(widget: QWidget, key: Qt.Key, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
Qt.NoModifier, delay: int = -1) - keyPress(widget: QWidget, key: str, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
Qt.NoModifier, delay: int = -1) - keyPress(window: QWindow, key: Qt.Key, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
Qt.NoModifier, delay: int = -1) - keyPress(window: QWindow, key: str, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
Qt.NoModifier, delay: int = -1)
- keyPress(widget: QWidget, key: Qt.Key, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
-
keyRelease
- keyRelease(widget: QWidget, key: Qt.Key, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
Qt.NoModifier, delay: int = -1) - keyRelease(widget: QWidget, key: str, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
Qt.NoModifier, delay: int = -1) - keyRelease(window: QWindow, key: Qt.Key, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
Qt.NoModifier, delay: int = -1) - keyRelease(window: QWindow, key: str, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
Qt.NoModifier, delay: int = -1)
- keyRelease(widget: QWidget, key: Qt.Key, modifier: Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] =
-
keySequence
- keySequence(widget: QWidget, keySequence: Union[QKeySequence, QKeySequence.StandardKey, str, int])
- keySequence(window: QWindow, keySequence: Union[QKeySequence, QKeySequence.StandardKey, str, int])
-
-
mouse开头的函数
-
mouseClick
- mouseClick(widget: QWidget, button: Qt.MouseButton, modifier:
Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] = Qt.KeyboardModifiers(), pos: QPoint = QPoint(), delay:
int = -1) - mouseClick(window: QWindow, button: Qt.MouseButton, modifier:
Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] = Qt.KeyboardModifiers(), pos: QPoint = QPoint(), delay:
int = -1)
- mouseClick(widget: QWidget, button: Qt.MouseButton, modifier:
-
mouseDClick
- mouseDClick(widget: QWidget, button: Qt.MouseButton, modifier:
Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] = Qt.KeyboardModifiers(), pos: QPoint = QPoint(), delay:
int = -1) - mouseDClick(window: QWindow, button: Qt.MouseButton, modifier:
Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] = Qt.KeyboardModifiers(), pos: QPoint = QPoint(), delay:
int = -1)
- mouseDClick(widget: QWidget, button: Qt.MouseButton, modifier:
-
mouseMove
- mouseMove(widget: QWidget, pos: QPoint = QPoint(), delay: int = -1)
- mouseMove(window: QWindow, pos: QPoint = QPoint(), delay: int = -1)
-
mousePress
- mousePress(widget: QWidget, button: Qt.MouseButton, modifier:
Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] = Qt.KeyboardModifiers(), pos: QPoint = QPoint(), delay:
int = -1) - mousePress(window: QWindow, button: Qt.MouseButton, modifier:
Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] = Qt.KeyboardModifiers(), pos: QPoint = QPoint(), delay:
int = -1)
- mousePress(widget: QWidget, button: Qt.MouseButton, modifier:
-
mouseRelease
- mouseRelease(widget: QWidget, button: Qt.MouseButton, modifier:
Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] = Qt.KeyboardModifiers(), pos: QPoint = QPoint(), delay:
int = -1) - mouseRelease(window: QWindow, button: Qt.MouseButton, modifier:
Union[Qt.KeyboardModifiers, Qt.KeyboardModifier] = Qt.KeyboardModifiers(), pos: QPoint = QPoint(), delay:
int = -1)
- mouseRelease(widget: QWidget, button: Qt.MouseButton, modifier:
-
-
touch开头的函数
- touchEvent
- touchEvent(widget: QWidget, device: QTouchDevice) -> QTest.QTouchEventSequence
- touchEvent(window: QWindow, device: QTouchDevice) -> QTest.QTouchEventSequence
- touchEvent
内置的枚举类, KeyAction代表了按键的动作类型。
class KeyAction(int):
Press = ... # type: QTest.KeyAction
Release = ... # type: QTest.KeyAction
Click = ... # type: QTest.KeyAction
Shortcut = ... # type: QTest.KeyAction
QTouchEventSequence是触控板事件,这里不做详细介绍。
信号测试
QtTest还提供了一个信号测试的类, QTest.SignalSpy,它可以用来测试信号的触发情况。
class QSignalSpy(QtCore.QObject):
@typing.overload
def __init__(self, signal: pyqtBoundSignal) -> None: ...
@typing.overload
def __init__(self, obj: QtCore.QObject, signal: QtCore.QMetaMethod) -> None: ...
def __delitem__(self, i: int) -> None: ...
def __setitem__(self, i: int, value: typing.Iterable[typing.Any]) -> None: ...
def __getitem__(self, i: int) -> typing.List[typing.Any]: ...
def __len__(self) -> int: ...
def wait(self, timeout: int = ...) -> bool: ...
def signal(self) -> QtCore.QByteArray: ...
def isValid(self) -> bool: ...
这个类跟Qt5中的实现由一些不同。因为在Python中,列表的接口是很容易通过魔术函数实现的,所以这里的QSignalSpy类就是一个两层列表,它的元素本身也是一个列表。
这个类有两个构造函数,一个是传入一个信号,一个是传入一个QObject和信号的元方法。这两个构造函数都会返回一个QSignalSpy对象。第一个就是用于通过@pyqtSignal定义的信号,这个信号必须与一个对象绑定。
第二个构造函数也挺有意思,把绑定的对象与信号分开,在PyQt5中要获得这个QMetaMethod对象,需要通过QMetaObject类的方法来获取元对象,然后在元对象中查找信号的元方法。这里的QMetaMethod对象是通过信号的元方法来获取的。
最平凡的例子
这里用一个很无趣的例子来说明QtTest的使用方法。
import sys
from functools import partial
from PyQt5 import QtTest
from PyQt5.QtCore import pyqtSignal, QObject, Qt
from PyQt5.QtTest import QSignalSpy
from PyQt5.QtWidgets import QApplication, QPushButton
class SmokeQtTest(QObject):
smoke = pyqtSignal(str)
def dump_signal(spy: QSignalSpy):
print(spy.signal(), ' signal count:', len(spy))
for i in range(len(spy)):
print("\t", spy[i])
if __name__ == '__main__':
app = QApplication(sys.argv)
smoke_test = SmokeQtTest()
spy = QtTest.QSignalSpy(smoke_test.smoke)
smoke_test.smoke.connect(print)
smoke_test.smoke.emit('Hello, World!')
widget = QPushButton('Click me')
widget.clicked.connect(partial(dump_signal, spy))
# 直接用第一种方法就行,实在是Copilot很无聊,自动生成了第二种方法
# button_spy = QSignalSpy(widget.clicked)
button_spy = QSignalSpy(widget,
widget.metaObject().method(widget.metaObject().indexOfSignal("clicked(bool)")))
widget.clicked.connect(partial(dump_signal, button_spy))
widget.show()
QtTest.QTest.qWaitForWindowExposed(widget)
QtTest.QTest.mouseClick(widget, Qt.LeftButton)
sys.exit(app.exec_())
程序运行后,会弹出一个按钮,点击按钮后,会打印出信号的内容。
Hello, World!
b'smoke(QString)' signal count: 1
['Hello, World!']
b'clicked(bool)' signal count: 1
[False]
这里有两个SigalSpy,一个监听自定义的信号,一个监听clicked信号。后面用QTest来模拟点击按钮的事件。
unittest框架
unittest框架是Python自带的一个单元测试框架,它的使用方法跟QtTest类似,也是通过继承TestCase类来实现的。它的使用方法也是通过assertEqual、assertTrue等来实现的。
import unittest
class SmokeTest(unittest.TestCase):
def setUp(self) -> None:
pass
def tearDown(self) -> None:
pass
def test_equal(self):
self.assertEqual(1, 1)
def test_true(self):
self.assertTrue(True)
TestCase类的setUp和tearDown方法分别是在每个测试用例开始和结束时调用的。这两个方法可以用来做一些初始化和清理工作。以及测试用例的方法必须以test开头,否则unittest框架不会执行。TestCase提供了很多的断言方法,例如assertEqual、assertTrue、assertIn等,这些方法都是用来判断测试结果是否符合预期的。
运行测试用例的方法有两种,一种是通过unittest.main()来运行,另一种是通过TestSuite来运行。
if __name__ == '__main__':
unittest.main()
这种方法会运行所有的测试用例,如果想要运行指定的测试用例,可以通过TestSuite来实现。
if __name__ == '__main__':
suite = unittest.TestSuite()
suite.addTest(SmokeTest('test_equal'))
suite.addTest(SmokeTest('test_true'))
runner = unittest.TextTestRunner()
runner.run(suite)
这里是手动添加了两个测试用例,然后通过TextTestRunner来运行。TextTestRunner是一个文本运行器,它会将测试结果输出到控制台。除了这个运行器,还有HTMLTestRunner、XMLTestRunner等,它们会将测试结果输出到HTML或者XML文件中。
还可以通过suite = unittest.TestLoader().loadTestsFromTestCase(SmokeTest)
来加载测试用例,这样就不需要手动添加了。
以HTMLTestRunner为例,它的使用方法如下:
# HTMLTestRunner = __import__('HTMLTestRunner')
runner = HTMLTestRunner(output='report', report_name='report', add_timestamp=True, combine_reports=True)
# unittest.TextTestRunner(verbosity=2).run(suite)
runner.run(suite)
当然首先要通过pip install html-testRunner
安装这个库。这里的output是输出的目录,report_name是报告的名称,add_timestamp是是否添加时间戳,combine_reports是是否合并报告。
然后运行python unittest_smoke.py
,就会在当前目录下生成一个report目录,里面有一个report-xxxxxxxx.html文件,打开这个文件就可以看到测试结果了。
注意这里不要运行python -m unittest unittest_smoke.py
,unittest会采用默认的运行期来运行默认的测试用例。
总结
- QTest的职责是模拟UI中的事件,例如鼠标点击、键盘输入等。
- QSignalSpy可用于监听信号的发射,然后获取信号的参数。
- unittest测试框架组织自动化测试。
- HTMLTestRunner将测试结果输出到HTML文件中。
下一章将完整编写一个PyQt5程序的测试驱动的开发。