PyQt5快速开发与实战
文章目录
9. 第9章 PyQt5 扩展应用
9.7 UI层的自动化测试
一般来说,UI层的自动化测试是通过工具或编写脚本的方式来模拟手工测试的过程,通过运行脚本来执行测试用例,从而模拟人工对软件的功能进行验证。
PyQt是Qt框架的Python语言实现,对于单元测试,Python可以使用它内部自带的单元测试模块unittest。对于模拟手工操作,PyQt可以使用它内部的测试模块QTest。
9.7.1 手工测试与自动化测试
特点总结:
- 手工测试由人手工去执行测试用例;自动化测试由程序代替人去执行测试用例。
- 手工测试非常消耗时间,持续进行手工测试会使测试人员感到疲惫;自动化测试可以代替一部分机械重复的手工测试。
- 手工测试永远无法被自动化测试取代。在整个软件开发周期中,手工测试发现 Bug所占的比例大,大约为80%;而自动化测试只能发现大约20%的 Bug。
- 手工测试适合测试业务逻辑;自动化测试适合进行回归测试。回归测试用于测试已有功能,而不是新增功能。自动化测试有利于测试项目底层的细节,比如可以测试出软件的崩溃、API的错误返回值、业务逻辑异常和软件的内存使用等。
9.7.2 模拟鸡尾酒的调酒器窗口
ui转py
# -*- coding: utf-8 -*-# Form implementation generated from reading ui file 'MatrixWinUi.ui'## Created by: PyQt5 UI code generator 5.9.2## WARNING! All changes made in this file will be lost!from PyQt5 import QtCore, QtGui, QtWidgets
classUi_MatrixWin(object):defsetupUi(self, MatrixWin):
MatrixWin.setObjectName("MatrixWin")
MatrixWin.resize(742,461)
self.groupBox = QtWidgets.QGroupBox(MatrixWin)
self.groupBox.setGeometry(QtCore.QRect(10,210,451,191))
self.groupBox.setObjectName("groupBox")
self.gridLayout_2 = QtWidgets.QGridLayout(self.groupBox)
self.gridLayout_2.setObjectName("gridLayout_2")
self.speedButton1 = QtWidgets.QRadioButton(self.groupBox)
self.speedButton1.setObjectName("speedButton1")
self.speedButtonGroup = QtWidgets.QButtonGroup(MatrixWin)
self.speedButtonGroup.setObjectName("speedButtonGroup")
self.speedButtonGroup.addButton(self.speedButton1)
self.gridLayout_2.addWidget(self.speedButton1,0,0,1,1)
self.speedButton3 = QtWidgets.QRadioButton(self.groupBox)
self.speedButton3.setObjectName("speedButton3")
self.speedButtonGroup.addButton(self.speedButton3)
self.gridLayout_2.addWidget(self.speedButton3,0,2,1,1)
self.speedButton4 = QtWidgets.QRadioButton(self.groupBox)
self.speedButton4.setObjectName("speedButton4")
self.speedButtonGroup.addButton(self.speedButton4)
self.gridLayout_2.addWidget(self.speedButton4,1,0,1,1)
self.speedButton5 = QtWidgets.QRadioButton(self.groupBox)
self.speedButton5.setChecked(True)
self.speedButton5.setObjectName("speedButton5")
self.speedButtonGroup.addButton(self.speedButton5)
self.gridLayout_2.addWidget(self.speedButton5,1,1,1,1)
self.speedButton6 = QtWidgets.QRadioButton(self.groupBox)
self.speedButton6.setObjectName("speedButton6")
self.speedButtonGroup.addButton(self.speedButton6)
self.gridLayout_2.addWidget(self.speedButton6,1,2,1,1)
self.speedButton9 = QtWidgets.QRadioButton(self.groupBox)
self.speedButton9.setObjectName("speedButton9")
self.speedButtonGroup.addButton(self.speedButton9)
self.gridLayout_2.addWidget(self.speedButton9,3,2,1,1)
self.speedButton8 = QtWidgets.QRadioButton(self.groupBox)
self.speedButton8.setObjectName("speedButton8")
self.speedButtonGroup.addButton(self.speedButton8)
self.gridLayout_2.addWidget(self.speedButton8,3,1,1,1)
self.speedButton7 = QtWidgets.QRadioButton(self.groupBox)
self.speedButton7.setObjectName("speedButton7")
self.speedButtonGroup.addButton(self.speedButton7)
self.gridLayout_2.addWidget(self.speedButton7,3,0,1,1)
self.speedButton2 = QtWidgets.QRadioButton(self.groupBox)
self.speedButton2.setObjectName("speedButton2")
self.speedButtonGroup.addButton(self.speedButton2)
self.gridLayout_2.addWidget(self.speedButton2,0,1,1,1)
self.resultGroup = QtWidgets.QGroupBox(MatrixWin)
self.resultGroup.setGeometry(QtCore.QRect(470,210,261,191))
self.resultGroup.setObjectName("resultGroup")
self.resultText = QtWidgets.QTextEdit(self.resultGroup)
self.resultText.setGeometry(QtCore.QRect(10,20,241,161))
self.resultText.setObjectName("resultText")
self.layoutWidget = QtWidgets.QWidget(MatrixWin)
self.layoutWidget.setGeometry(QtCore.QRect(10,420,390,30))
self.layoutWidget.setObjectName("layoutWidget")
self.horizontalLayout = QtWidgets.QHBoxLayout(self.layoutWidget)
self.horizontalLayout.setContentsMargins(0,0,0,0)
self.horizontalLayout.setObjectName("horizontalLayout")
self.okBtn = QtWidgets.QPushButton(self.layoutWidget)
self.okBtn.setObjectName("okBtn")
self.horizontalLayout.addWidget(self.okBtn)
self.clearBtn = QtWidgets.QPushButton(self.layoutWidget)
self.clearBtn.setObjectName("clearBtn")
self.horizontalLayout.addWidget(self.clearBtn)
self.cancelBtn = QtWidgets.QPushButton(self.layoutWidget)
self.cancelBtn.setObjectName("cancelBtn")
self.horizontalLayout.addWidget(self.cancelBtn)
self.groupBox_2 = QtWidgets.QGroupBox(MatrixWin)
self.groupBox_2.setGeometry(QtCore.QRect(10,10,721,191))
self.groupBox_2.setObjectName("groupBox_2")
self.label = QtWidgets.QLabel(self.groupBox_2)
self.label.setGeometry(QtCore.QRect(20,30,151,21))
self.label.setObjectName("label")
self.label_2 = QtWidgets.QLabel(self.groupBox_2)
self.label_2.setGeometry(QtCore.QRect(20,60,151,21))
self.label_2.setObjectName("label_2")
self.label_7 = QtWidgets.QLabel(self.groupBox_2)
self.label_7.setGeometry(QtCore.QRect(20,90,131,21))
self.label_7.setObjectName("label_7")
self.label_4 = QtWidgets.QLabel(self.groupBox_2)
self.label_4.setGeometry(QtCore.QRect(20,120,151,22))
self.label_4.setObjectName("label_4")
self.tequilaScrollBar = QtWidgets.QScrollBar(self.groupBox_2)
self.tequilaScrollBar.setEnabled(True)
self.tequilaScrollBar.setGeometry(QtCore.QRect(130,30,361,21))
self.tequilaScrollBar.setMaximum(11)
self.tequilaScrollBar.setProperty("value",8)
self.tequilaScrollBar.setSliderPosition(8)
self.tequilaScrollBar.setOrientation(QtCore.Qt.Horizontal)
self.tequilaScrollBar.setObjectName("tequilaScrollBar")
self.tripleSecSpinBox = QtWidgets.QSpinBox(self.groupBox_2)
self.tripleSecSpinBox.setGeometry(QtCore.QRect(130,60,250,21))
self.tripleSecSpinBox.setMaximum(11)
self.tripleSecSpinBox.setProperty("value",4)
self.tripleSecSpinBox.setObjectName("tripleSecSpinBox")
self.limeJuiceLineEdit = QtWidgets.QLineEdit(self.groupBox_2)
self.limeJuiceLineEdit.setGeometry(QtCore.QRect(130,90,257,21))
self.limeJuiceLineEdit.setObjectName("limeJuiceLineEdit")
self.iceHorizontalSlider = QtWidgets.QSlider(self.groupBox_2)
self.iceHorizontalSlider.setGeometry(QtCore.QRect(130,120,250,22))
self.iceHorizontalSlider.setMinimum(0)
self.iceHorizontalSlider.setMaximum(20)
self.iceHorizontalSlider.setProperty("value",12)
self.iceHorizontalSlider.setOrientation(QtCore.Qt.Horizontal)
self.iceHorizontalSlider.setObjectName("iceHorizontalSlider")
self.label_6 = QtWidgets.QLabel(self.groupBox_2)
self.label_6.setGeometry(QtCore.QRect(610,30,61,21))
self.label_6.setObjectName("label_6")
self.label_3 = QtWidgets.QLabel(self.groupBox_2)
self.label_3.setGeometry(QtCore.QRect(610,50,61,21))
self.label_3.setObjectName("label_3")
self.label_8 = QtWidgets.QLabel(self.groupBox_2)
self.label_8.setGeometry(QtCore.QRect(610,80,61,21))
self.label_8.setObjectName("label_8")
self.label_5 = QtWidgets.QLabel(self.groupBox_2)
self.label_5.setGeometry(QtCore.QRect(610,120,61,21))
self.label_5.setObjectName("label_5")
self.selScrollBarLbl = QtWidgets.QLabel(self.groupBox_2)
self.selScrollBarLbl.setGeometry(QtCore.QRect(520,30,51,21))
self.selScrollBarLbl.setText("")
self.selScrollBarLbl.setObjectName("selScrollBarLbl")
self.selIceSliderLbl = QtWidgets.QLabel(self.groupBox_2)
self.selIceSliderLbl.setGeometry(QtCore.QRect(520,120,51,21))
self.selIceSliderLbl.setText("")
self.selIceSliderLbl.setObjectName("selIceSliderLbl")
self.retranslateUi(MatrixWin)
self.okBtn.clicked.connect(MatrixWin.uiAccept)
self.cancelBtn.clicked.connect(MatrixWin.uiReject)
self.clearBtn.clicked.connect(MatrixWin.uiClear)
self.iceHorizontalSlider.valueChanged['int'].connect(MatrixWin.uiIceSliderValueChanged)
self.tequilaScrollBar.valueChanged['int'].connect(MatrixWin.uiScrollBarValueChanged)
QtCore.QMetaObject.connectSlotsByName(MatrixWin)defretranslateUi(self, MatrixWin):
_translate = QtCore.QCoreApplication.translate
MatrixWin.setWindowTitle(_translate("MatrixWin","玛格丽特鸡尾酒*调酒器"))
self.groupBox.setToolTip(_translate("MatrixWin","Speed of the blender"))
self.groupBox.setTitle(_translate("MatrixWin","9种搅拌速度"))
self.speedButton1.setText(_translate("MatrixWin","&Mix"))
self.speedButton3.setText(_translate("MatrixWin","&Puree"))
self.speedButton4.setText(_translate("MatrixWin","&Chop"))
self.speedButton5.setText(_translate("MatrixWin","&Karate Chop"))
self.speedButton6.setText(_translate("MatrixWin","&Beat"))
self.speedButton9.setText(_translate("MatrixWin","&Vaporize"))
self.speedButton8.setText(_translate("MatrixWin","&Liquefy"))
self.speedButton7.setText(_translate("MatrixWin","&Smash"))
self.speedButton2.setText(_translate("MatrixWin","&Whip"))
self.resultGroup.setTitle(_translate("MatrixWin","操作结果"))
self.okBtn.setText(_translate("MatrixWin","OK"))
self.clearBtn.setText(_translate("MatrixWin","Clear"))
self.cancelBtn.setText(_translate("MatrixWin","Cancel"))
self.groupBox_2.setTitle(_translate("MatrixWin","原料"))
self.label.setText(_translate("MatrixWin","龙舌兰酒"))
self.label_2.setText(_translate("MatrixWin","三重蒸馏酒"))
self.label_7.setText(_translate("MatrixWin","柠檬汁"))
self.label_4.setText(_translate("MatrixWin","冰块"))
self.tequilaScrollBar.setToolTip(_translate("MatrixWin","Jiggers of tequila"))
self.tripleSecSpinBox.setToolTip(_translate("MatrixWin","Jiggers of triple sec"))
self.limeJuiceLineEdit.setToolTip(_translate("MatrixWin","Jiggers of lime juice"))
self.limeJuiceLineEdit.setText(_translate("MatrixWin","12.0"))
self.iceHorizontalSlider.setToolTip(_translate("MatrixWin","Chunks of ice"))
self.label_6.setText(_translate("MatrixWin","升"))
self.label_3.setText(_translate("MatrixWin","升"))
self.label_8.setText(_translate("MatrixWin","升"))
self.label_5.setText(_translate("MatrixWin","个"))
调用主窗口
# -*- coding: utf-8 -*-import sys
from PyQt5.QtWidgets import*from MatrixWinUi import*classCallMatrixWinUi(QWidget ):def__init__(self, parent=None):super(CallMatrixWinUi, self).__init__(parent)
self.ui = Ui_MatrixWin()
self.ui.setupUi(self)
self.initUi()# 初始化窗口 definitUi(self):
scrollVal = self.ui.tequilaScrollBar.value()
self.ui.selScrollBarLbl.setText(str(scrollVal))
sliderVal = self.ui.iceHorizontalSlider.value()
self.ui.selIceSliderLbl.setText(str(sliderVal))# 获得一量杯酒的重量,单位:克defgetJiggers(self):# 返回玛格丽特就得总容量,以jigger量酒器为单位。# 一个量酒器可以容纳0.0444升的酒。
jiggersTequila = self.ui.tequilaScrollBar.value()
jiggersTripleSec = self.ui.tripleSecSpinBox.value()
jiggersLimeJuice =float(self.ui.limeJuiceLineEdit.text())
jiggersIce = self.ui.iceHorizontalSlider.value()return jiggersTequila + jiggersTripleSec + jiggersLimeJuice + jiggersIce
# 获得一量杯酒的体积,单位:升defgetLiters(self):'''返回鸡尾酒的总容量(升)'''return0.0444* self.getJiggers()# 获得搅拌速度defgetSpeedName(self):
speedButton = self.ui.speedButtonGroup.checkedButton()if speedButton isNone:returnNonereturn speedButton.text()# 点击ok按钮后,把响应的结果显示在resultText文本框里 defuiAccept(self):print('* CallMatrixWinUi accept ')print('The volume of drinks is {0} liters ({1} jiggers).'.format(self.getLiters(), self.getJiggers()))print('The blender is running at speed "{0}"'.format(self.getSpeedName()))
msg1 ='饮料量为: {0} 升 ({1} 个量酒器)。'.format(self.getLiters(), self.getJiggers())
msg2 ='调酒器的搅拌速度是: "{0}"。'.format(self.getSpeedName())
self.ui.resultText.clear()
self.ui.resultText.append(msg1)
self.ui.resultText.append(msg2)# 点击cancel按钮,关闭窗口 defuiReject(self):print('* CallMatrixWinUi reject ')'''Cancel.'''
self.close()# 点击clear按钮,清空操作结果 defuiClear(self):print('* CallMatrixWinUi uiClear ')
self.ui.resultText.clear()defuiScrollBarValueChanged(self):print('* uiScrollBarValueChanged ---------')
pos = self.ui.tequilaScrollBar.value()
self.ui.selScrollBarLbl.setText(str(pos))defuiIceSliderValueChanged( self):print('* uiIceSliderValueChanged ---------')
pos = self.ui.iceHorizontalSlider.value()
self.ui.selIceSliderLbl.setText(str(pos))if __name__=="__main__":from pyqt5_plugins.examples.exampleqmlitem import QtCore
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
app = QApplication(sys.argv)
demo = CallMatrixWinUi()
demo.show()
sys.exit(app.exec_())
运行
9.7.3 单元测试程序
- 编写单元测试类
classMatrixWinTest(unittest.TestCase):# 初始化工作 defsetUp(self):print('*** setUp ***') self.app = QApplication(sys.argv) self.form = CallMatrixWinUi.CallMatrixWinUi() self.form.show()# 新建对象,传入参数。每5秒执行一个测试用例 TestCase。 self.bkThread = BackWorkThread(int(5))# 连接子进程的信号和槽函数 self.bkThread.finishSignal.connect(self.closeWindow)#self.bkThread.finishSignal.connect(self.app.exec_)# 启动线程,开始执行run()函数里的内容 self.bkThread.start()# 退出清理工作 deftearDown(self):print('*** tearDown ***') self.app.exec_()
- 定时关闭窗口
# 继承 QThread 类classBackWorkThread(QThread):# 声明一个信号,同时返回一个str finishSignal = pyqtSignal(str)# 构造函数里增加形参def__init__(self, sleepTime,parent=None):super(BackWorkThread, self).__init__(parent)# 储存参数 self.sleepTime = sleepTime #重写run()函数,在里面定时执行业务。defrun(self):# 休眠一段时间 time.sleep(self.sleepTime)# 休眠结束,发送一个信号告诉主线程窗口 self.finishSignal.emit('ok , begin to close Window')
- 测试调酒器窗口的默认值
# 测试用例-在默认状态下的测试GUI deftest_defaults(self):'''测试GUI处于默认状态'''print('*** testCase test_defaults begin ***')
self.form.setWindowTitle('开始测试用例 test_defaults ')
self.assertEqual(self.form.ui.tequilaScrollBar.value(),8)
self.assertEqual(self.form.ui.tripleSecSpinBox.value(),4)
self.assertEqual(self.form.ui.limeJuiceLineEdit.text(),"12.0")
self.assertEqual(self.form.ui.iceHorizontalSlider.value(),12)
self.assertEqual(self.form.ui.speedButtonGroup.checkedButton().text(),"&Karate Chop")print('*** speedName='+ self.form.getSpeedName())# 用鼠标左键按OK
okWidget = self.form.ui.okBtn
QTest.mouseClick(okWidget, Qt.LeftButton)# 即使没有按OK,Class也处于默认状态
self.assertEqual(self.form.getJiggers(),36.0)
self.assertEqual(self.form.getSpeedName(),"&Karate Chop")print('*** testCase test_defaults end ***')
- 测试PyQt的QScrollBar
# 设置窗口中所有部件的值为0,状态为初始状态。 defsetFormToZero(self):print('* setFormToZero *') self.form.ui.tequilaScrollBar.setValue(0) self.form.ui.tripleSecSpinBox.setValue(0) self.form.ui.limeJuiceLineEdit.setText("0.0") self.form.ui.iceHorizontalSlider.setValue(0) self.form.ui.selScrollBarLbl.setText("0") self.form.ui.selIceSliderLbl.setText("0")``````# 测试用例-测试滚动条deftest_moveScrollBar(self):'''测试用例test_moveScrollBar'''print('*** testCase test_moveScrollBar begin ***') self.form.setWindowTitle('开始测试用例 test_moveScrollBar ') self.setFormToZero()# 测试将龙舌兰酒的滚动条的值设定为 12 ,ui中它实际的最大值为 11 self.form.ui.tequilaScrollBar.setValue(12)print('* 当执行self.form.ui.tequilaScrollBar.setValue(12) 后,ui.tequilaScrollBar.value() => '+str( self.form.ui.tequilaScrollBar.value())) self.assertEqual(self.form.ui.tequilaScrollBar.value(),11)# 测试将龙舌兰酒的滚动条的值设定为 -1 ,ui中它实际的最小值为 0 self.form.ui.tequilaScrollBar.setValue(-1)print('* 当执行self.form.ui.tequilaScrollBar.setValue(-1) 后,ui.tequilaScrollBar.value() => '+str( self.form.ui.tequilaScrollBar.value())) self.assertEqual(self.form.ui.tequilaScrollBar.value(),0)# 重新将将龙舌兰酒的滚动条的值设定为 5 self.form.ui.tequilaScrollBar.setValue(5)# 用鼠标左键按OK按钮 okWidget = self.form.ui.okBtn QTest.mouseClick(okWidget, Qt.LeftButton) self.assertEqual(self.form.getJiggers(),5)print('*** testCase test_moveScrollBar end ***')
- 测试PyQt的QSpinBox
# 测试用例-测试滚动条deftest_tripleSecSpinBox(self):'''测试用例 test_tripleSecSpinBox '''print('*** testCase test_tripleSecSpinBox begin ***') self.form.setWindowTitle('开始测试用例 test_tripleSecSpinBox ')'''测试修改spinBox部件的最大最小值 测试它的最小和最大值作为读者的练习。 ''' self.setFormToZero()# tripleSecSpinBox在界面中的取值范围为 0 到 11, 将它的最大值设为 12,看是否显示正常。 self.form.ui.tripleSecSpinBox.setValue(12)print('* 当执行self.form.ui.tripleSecSpinBox.setValue(12) 后,ui.tripleSecSpinBox.value() => '+str( self.form.ui.tripleSecSpinBox.value())) self.assertEqual(self.form.ui.tripleSecSpinBox.value(),11)# tripleSecSpinBox在界面中的取值范围为 0 到 11, 将它的最小值设为 -1, 看是否显示正常。 self.form.ui.tripleSecSpinBox.setValue(-1)print('* 当执行self.form.ui.tripleSecSpinBox.setValue(-1) 后,ui.tripleSecSpinBox.value() => '+str( self.form.ui.tripleSecSpinBox.value())) self.assertEqual(self.form.ui.tripleSecSpinBox.value(),0) self.form.ui.tripleSecSpinBox.setValue(2)# 用鼠标左键按OK按钮 okWidget = self.form.ui.okBtn QTest.mouseClick(okWidget, Qt.LeftButton) self.assertEqual(self.form.getJiggers(),2)print('*** testCase test_tripleSecSpinBox end ***')
- 测试PyQt的QLineEdit
# 测试用例-测试柠檬汁单行文本框 deftest_limeJuiceLineEdit(self):'''测试用例 test_limeJuiceLineEdit '''print('*** testCase test_limeJuiceLineEdit begin ***') self.form.setWindowTitle('开始测试用例 test_limeJuiceLineEdit ')'''测试修改juice line edit部件的最大最小值 测试它的最小和最大值作为读者的练习。 ''' self.setFormToZero()# 清除lineEdit小部件值,然后在lineEdit小部件中键入“3.5” self.form.ui.limeJuiceLineEdit.clear() QTest.keyClicks(self.form.ui.limeJuiceLineEdit,"3.5")# 用鼠标左键按OK按钮 okWidget = self.form.ui.okBtn QTest.mouseClick(okWidget, Qt.LeftButton) self.assertEqual(self.form.getJiggers(),3.5)print('*** testCase test_limeJuiceLineEdit end ***')
- 测试PyQt的QSlider
# 测试用例-测试iceHorizontalSliderdeftest_iceHorizontalSlider(self):'''测试用例 test_iceHorizontalSlider '''print('*** testCase test_iceHorizontalSlider begin ***') self.form.setWindowTitle('开始测试用例 test_iceHorizontalSlider ')'''测试ice slider. 测试它的最小和最大值作为读者的练习。 ''' self.setFormToZero() self.form.ui.iceHorizontalSlider.setValue(4)# 用鼠标左键按OK按钮 okWidget = self.form.ui.okBtn QTest.mouseClick(okWidget, Qt.LeftButton) self.assertEqual(self.form.getJiggers(),4)print('*** testCase test_iceHorizontalSlider end ***')
- 测试PyQt的QRadioButton
deftest_blenderSpeedButtons(self):print('*** testCase test_blenderSpeedButtons begin ***')'''测试选择搅拌速度按钮''' self.form.ui.speedButton1.click() self.assertEqual(self.form.getSpeedName(),"&Mix") self.form.ui.speedButton2.click() self.assertEqual(self.form.getSpeedName(),"&Whip") self.form.ui.speedButton3.click() self.assertEqual(self.form.getSpeedName(),"&Puree") self.form.ui.speedButton4.click() self.assertEqual(self.form.getSpeedName(),"&Chop") self.form.ui.speedButton5.click() self.assertEqual(self.form.getSpeedName(),"&Karate Chop") self.form.ui.speedButton6.click() self.assertEqual(self.form.getSpeedName(),"&Beat") self.form.ui.speedButton7.click() self.assertEqual(self.form.getSpeedName(),"&Smash") self.form.ui.speedButton8.click() self.assertEqual(self.form.getSpeedName(),"&Liquefy") self.form.ui.speedButton9.click() self.assertEqual(self.form.getSpeedName(),"&Vaporize")print('*** testCase test_blenderSpeedButtons end ***')
9.7.4 运行测试用例
- 默认执行所有的测试用例
- 按照指定顺序执行测试用例
9.7.5 生成测试报告
使用HTMLTestRunner生成测试报告。
HTMLTestRunner.py
"""
A TestRunner for use with the Python unit testing framework. It
generates a HTML report to show the result at a glance.
The simplest way to use this is to invoke its main method. E.g.
import unittest
import HTMLTestRunner
... define your tests ...
if __name__ == '__main__':
HTMLTestRunner.main()
For more customization options, instantiates a HTMLTestRunner object.
HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.
# output to a file
fp = file('my_report.html', 'wb')
runner = HTMLTestRunner.HTMLTestRunner(
stream=fp,
title='My unit test',
description='This demonstrates the report output by HTMLTestRunner.'
)
# Use an external stylesheet.
# See the Template_mixin class for more customizable options
runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
# run the test
runner.run(my_test_suite)
------------------------------------------------------------------------
Copyright (c) 2004-2007, Wai Yip Tung
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name Wai Yip Tung nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""# URL: http://tungwaiyip.info/software/HTMLTestRunner.html
__author__ ="Wai Yip Tung"
__version__ ="0.8.2""""
Change History
Version 0.8.2
* Show output inline instead of popup window (Viorel Lupu).
Version in 0.8.1
* Validated XHTML (Wolfgang Borgert).
* Added description of test classes and test cases.
Version in 0.8.0
* Define Template_mixin class for customization.
* Workaround a IE 6 bug that it does not treat <script> block as CDATA.
Version in 0.7.1
* Back port to Python 2.3 (Frank Horowitz).
* Fix missing scroll bars in detail log (Podi).
"""# TODO: color stderr# TODO: simplify javascript using ,ore than 1 class in the class attribute?import datetime
import io
import sys
import time
import unittest
from xml.sax import saxutils
# ------------------------------------------------------------------------# The redirectors below are used to capture output during testing. Output# sent to sys.stdout and sys.stderr are automatically captured. However# in some cases sys.stdout is already cached before HTMLTestRunner is# invoked (e.g. calling logging.basicConfig). In order to capture those# output, use the redirectors for the cached stream.## e.g.# >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)# >>>classOutputRedirector(object):""" Wrapper to redirect stdout or stderr """def__init__(self, fp):
self.fp = fp
defwrite(self, s):
self.fp.write(s)defwritelines(self, lines):
self.fp.writelines(lines)defflush(self):
self.fp.flush()
stdout_redirector = OutputRedirector(sys.stdout)
stderr_redirector = OutputRedirector(sys.stderr)# ----------------------------------------------------------------------# TemplateclassTemplate_mixin(object):"""
Define a HTML template for report customerization and generation.
Overall structure of an HTML report
HTML
+------------------------+
|<html> |
| <head> |
| |
| STYLESHEET |
| +----------------+ |
| | | |
| +----------------+ |
| |
| </head> |
| |
| <body> |
| |
| HEADING |
| +----------------+ |
| | | |
| +----------------+ |
| |
| REPORT |
| +----------------+ |
| | | |
| +----------------+ |
| |
| ENDING |
| +----------------+ |
| | | |
| +----------------+ |
| |
| </body> |
|</html> |
+------------------------+
"""
STATUS ={0:'pass',1:'fail',2:'error',}
DEFAULT_TITLE ='Unit Test Report'
DEFAULT_DESCRIPTION =''# ------------------------------------------------------------------------# HTML Template
HTML_TMPL =r"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>%(title)s</title>
<meta name="generator" content="%(generator)s"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
%(stylesheet)s
</head>
<body>
<script language="javascript" type="text/javascript"><!--
output_list = Array();
/* level - 0:Summary; 1:Failed; 2:All */
function showCase(level) {
trs = document.getElementsByTagName("tr");
for (var i = 0; i < trs.length; i++) {
tr = trs[i];
id = tr.id;
if (id.substr(0,2) == 'ft') {
if (level < 1) {
tr.className = 'hiddenRow';
}
else {
tr.className = '';
}
}
if (id.substr(0,2) == 'pt') {
if (level > 1) {
tr.className = '';
}
else {
tr.className = 'hiddenRow';
}
}
}
}
function showClassDetail(cid, count) {
var id_list = Array(count);
var toHide = 1;
for (var i = 0; i < count; i++) {
tid0 = 't' + cid.substr(1) + '.' + (i+1);
tid = 'f' + tid0;
tr = document.getElementById(tid);
if (!tr) {
tid = 'p' + tid0;
tr = document.getElementById(tid);
}
id_list[i] = tid;
if (tr.className) {
toHide = 0;
}
}
for (var i = 0; i < count; i++) {
tid = id_list[i];
if (toHide) {
document.getElementById('div_'+tid).style.display = 'none'
document.getElementById(tid).className = 'hiddenRow';
}
else {
document.getElementById(tid).className = '';
}
}
}
function showTestDetail(div_id){
var details_div = document.getElementById(div_id)
var displayState = details_div.style.display
// alert(displayState)
if (displayState != 'block' ) {
displayState = 'block'
details_div.style.display = 'block'
}
else {
details_div.style.display = 'none'
}
}
function html_escape(s) {
s = s.replace(/&/g,'&');
s = s.replace(/</g,'<');
s = s.replace(/>/g,'>');
return s;
}
/* obsoleted by detail in <div>
function showOutput(id, name) {
var w = window.open("", //url
name,
"resizable,scrollbars,status,width=800,height=450");
d = w.document;
d.write("<pre>");
d.write(html_escape(output_list[id]));
d.write("\n");
d.write("<a href='javascript:window.close()'>close</a>\n");
d.write("</pre>\n");
d.close();
}
*/
--></script>
%(heading)s
%(report)s
%(ending)s
</body>
</html>
"""# variables: (title, generator, stylesheet, heading, report, ending)# ------------------------------------------------------------------------# Stylesheet## alternatively use a <link> for external style sheet, e.g.# <link rel="stylesheet" href="$url" type="text/css">
STYLESHEET_TMPL ="""
<style type="text/css" media="screen">
body { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; }
table { font-size: 100%; }
pre { }
/* -- heading ---------------------------------------------------------------------- */
h1 {
font-size: 16pt;
color: gray;
}
.heading {
margin-top: 0ex;
margin-bottom: 1ex;
}
.heading .attribute {
margin-top: 1ex;
margin-bottom: 0;
}
.heading .description {
margin-top: 4ex;
margin-bottom: 6ex;
}
/* -- css div popup ------------------------------------------------------------------------ */
a.popup_link {
}
a.popup_link:hover {
color: red;
}
.popup_window {
display: none;
position: relative;
left: 0px;
top: 0px;
/*border: solid #627173 1px; */
padding: 10px;
background-color: #E6E6D6;
font-family: "Lucida Console", "Courier New", Courier, monospace;
text-align: left;
font-size: 8pt;
width: 500px;
}
}
/* -- report ------------------------------------------------------------------------ */
#show_detail_line {
margin-top: 3ex;
margin-bottom: 1ex;
}
#result_table {
width: 80%;
border-collapse: collapse;
border: 1px solid #777;
}
#header_row {
font-weight: bold;
color: white;
background-color: #777;
}
#result_table td {
border: 1px solid #777;
padding: 2px;
}
#total_row { font-weight: bold; }
.passClass { background-color: #6c6; }
.failClass { background-color: #c60; }
.errorClass { background-color: #c00; }
.passCase { color: #6c6; }
.failCase { color: #c60; font-weight: bold; }
.errorCase { color: #c00; font-weight: bold; }
.hiddenRow { display: none; }
.testcase { margin-left: 2em; }
/* -- ending ---------------------------------------------------------------------- */
#ending {
}
</style>
"""# ------------------------------------------------------------------------# Heading#
HEADING_TMPL ="""<div class='heading'>
<h1>%(title)s</h1>
%(parameters)s
<p class='description'>%(description)s</p>
</div>
"""# variables: (title, parameters, description)
HEADING_ATTRIBUTE_TMPL ="""<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
"""# variables: (name, value)# ------------------------------------------------------------------------# Report#
REPORT_TMPL ="""
<p id='show_detail_line'>Show
<a href='javascript:showCase(0)'>Summary</a>
<a href='javascript:showCase(1)'>Failed</a>
<a href='javascript:showCase(2)'>All</a>
</p>
<table id='result_table'>
<colgroup>
<col align='left' />
<col align='right' />
<col align='right' />
<col align='right' />
<col align='right' />
<col align='right' />
</colgroup>
<tr id='header_row'>
<td>Test Group/Test case</td>
<td>Count</td>
<td>Pass</td>
<td>Fail</td>
<td>Error</td>
<td>View</td>
</tr>
%(test_list)s
<tr id='total_row'>
<td>Total</td>
<td>%(count)s</td>
<td>%(Pass)s</td>
<td>%(fail)s</td>
<td>%(error)s</td>
<td> </td>
</tr>
</table>
"""# variables: (test_list, count, Pass, fail, error)
REPORT_CLASS_TMPL =r"""
<tr class='%(style)s'>
<td>%(desc)s</td>
<td>%(count)s</td>
<td>%(Pass)s</td>
<td>%(fail)s</td>
<td>%(error)s</td>
<td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
</tr>
"""# variables: (style, desc, count, Pass, fail, error, cid)
REPORT_TEST_WITH_OUTPUT_TMPL =r"""
<tr id='%(tid)s' class='%(Class)s'>
<td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
<td colspan='5' align='center'>
<!--css div popup start-->
<a class="popup_link" οnfοcus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
%(status)s</a>
<div id='div_%(tid)s' class="popup_window">
<div style='text-align: right; color:red;cursor:pointer'>
<a οnfοcus='this.blur();' οnclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
[x]</a>
</div>
<pre>
%(script)s
</pre>
</div>
<!--css div popup end-->
</td>
</tr>
"""# variables: (tid, Class, style, desc, status)
REPORT_TEST_NO_OUTPUT_TMPL =r"""
<tr id='%(tid)s' class='%(Class)s'>
<td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
<td colspan='5' align='center'>%(status)s</td>
</tr>
"""# variables: (tid, Class, style, desc, status)
REPORT_TEST_OUTPUT_TMPL =r"""
%(id)s: %(output)s
"""# variables: (id, output)# ------------------------------------------------------------------------# ENDING#
ENDING_TMPL ="""<div id='ending'> </div>"""# -------------------- The end of the Template class -------------------
TestResult = unittest.TestResult
class_TestResult(TestResult):# note: _TestResult is a pure representation of results.# It lacks the output and reporting ability compares to unittest._TextTestResult.def__init__(self, verbosity=1):
TestResult.__init__(self)
self.stdout0 =None
self.stderr0 =None
self.success_count =0
self.failure_count =0
self.error_count =0
self.verbosity = verbosity
# result is a list of result in 4 tuple# (# result code (0: success; 1: fail; 2: error),# TestCase object,# Test output (byte string),# stack trace,# )
self.result =[]defstartTest(self, test):
TestResult.startTest(self, test)# just one buffer for both stdout and stderr
self.outputBuffer = io.StringIO()
stdout_redirector.fp = self.outputBuffer
stderr_redirector.fp = self.outputBuffer
self.stdout0 = sys.stdout
self.stderr0 = sys.stderr
sys.stdout = stdout_redirector
sys.stderr = stderr_redirector
defcomplete_output(self):"""
Disconnect output redirection and return buffer.
Safe to call multiple times.
"""if self.stdout0:
sys.stdout = self.stdout0
sys.stderr = self.stderr0
self.stdout0 =None
self.stderr0 =Nonereturn self.outputBuffer.getvalue()defstopTest(self, test):# Usually one of addSuccess, addError or addFailure would have been called.# But there are some path in unittest that would bypass this.# We must disconnect stdout in stopTest(), which is guaranteed to be called.
self.complete_output()defaddSuccess(self, test):
self.success_count +=1
TestResult.addSuccess(self, test)
output = self.complete_output()
self.result.append((0, test, output,''))if self.verbosity >1:
sys.stderr.write('ok ')
sys.stderr.write(str(test))
sys.stderr.write('\n')else:
sys.stderr.write('.')defaddError(self, test, err):
self.error_count +=1
TestResult.addError(self, test, err)
_, _exc_str = self.errors[-1]
output = self.complete_output()
self.result.append((2, test, output, _exc_str))if self.verbosity >1:
sys.stderr.write('E ')
sys.stderr.write(str(test))
sys.stderr.write('\n')else:
sys.stderr.write('E')defaddFailure(self, test, err):
self.failure_count +=1
TestResult.addFailure(self, test, err)
_, _exc_str = self.failures[-1]
output = self.complete_output()
self.result.append((1, test, output, _exc_str))if self.verbosity >1:
sys.stderr.write('F ')
sys.stderr.write(str(test))
sys.stderr.write('\n')else:
sys.stderr.write('F')classHTMLTestRunner(Template_mixin):"""
"""def__init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
self.stream = stream
self.verbosity = verbosity
if title isNone:
self.title = self.DEFAULT_TITLE
else:
self.title = title
if description isNone:
self.description = self.DEFAULT_DESCRIPTION
else:
self.description = description
self.startTime = datetime.datetime.now()defrun(self, test):"Run the given test case or test suite."
result = _TestResult(self.verbosity)
test(result)
self.stopTime = datetime.datetime.now()
self.generateReport(test, result)# print >> sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)print(sys.stderr,'\nTime Elapsed: %s'%(self.stopTime-self.startTime))return result
defsortResult(self, result_list):# unittest does not seems to run in any particular order.# Here at least we want to group them together by class.
rmap ={}
classes =[]for n,t,o,e in result_list:
cls = t.__class__
ifnot cls in rmap:
rmap[cls]=[]
classes.append(cls)
rmap[cls].append((n,t,o,e))
r =[(cls, rmap[cls])for cls in classes]return r
defgetReportAttributes(self, result):"""
Return report attributes as a list of (name, value).
Override this to add custom attributes.
"""
startTime =str(self.startTime)[:19]
duration =str(self.stopTime - self.startTime)
status =[]if result.success_count: status.append('Pass %s'% result.success_count)if result.failure_count: status.append('Failure %s'% result.failure_count)if result.error_count: status.append('Error %s'% result.error_count )if status:
status =' '.join(status)else:
status ='none'return[('Start Time', startTime),('Duration', duration),('Status', status),]defgenerateReport(self, test, result):
report_attrs = self.getReportAttributes(result)
generator ='HTMLTestRunner %s'% __version__
stylesheet = self._generate_stylesheet()
heading = self._generate_heading(report_attrs)
report = self._generate_report(result)
ending = self._generate_ending()
output = self.HTML_TMPL %dict(
title = saxutils.escape(self.title),
generator = generator,
stylesheet = stylesheet,
heading = heading,
report = report,
ending = ending,)
self.stream.write(output.encode('utf8'))def_generate_stylesheet(self):return self.STYLESHEET_TMPL
def_generate_heading(self, report_attrs):
a_lines =[]for name, value in report_attrs:
line = self.HEADING_ATTRIBUTE_TMPL %dict(
name = saxutils.escape(name),
value = saxutils.escape(value),)
a_lines.append(line)
heading = self.HEADING_TMPL %dict(
title = saxutils.escape(self.title),
parameters =''.join(a_lines),
description = saxutils.escape(self.description),)return heading
def_generate_report(self, result):
rows =[]
sortedResult = self.sortResult(result.result)for cid,(cls, cls_results)inenumerate(sortedResult):# subtotal for a class
np = nf = ne =0for n,t,o,e in cls_results:if n ==0: np +=1elif n ==1: nf +=1else: ne +=1# format class descriptionif cls.__module__ =="__main__":
name = cls.__name__
else:
name ="%s.%s"%(cls.__module__, cls.__name__)
doc = cls.__doc__ and cls.__doc__.split("\n")[0]or""
desc = doc and'%s: %s'%(name, doc)or name
row = self.REPORT_CLASS_TMPL %dict(
style = ne >0and'errorClass'or nf >0and'failClass'or'passClass',
desc = desc,
count = np+nf+ne,
Pass = np,
fail = nf,
error = ne,
cid ='c%s'%(cid+1),)
rows.append(row)for tid,(n,t,o,e)inenumerate(cls_results):
self._generate_report_test(rows, cid, tid, n, t, o, e)
report = self.REPORT_TMPL %dict(
test_list =''.join(rows),
count =str(result.success_count+result.failure_count+result.error_count),
Pass =str(result.success_count),
fail =str(result.failure_count),
error =str(result.error_count),)return report
def_generate_report_test(self, rows, cid, tid, n, t, o, e):# e.g. 'pt1.1', 'ft1.1', etc
has_output =bool(o or e)
tid =(n ==0and'p'or'f')+'t%s.%s'%(cid+1,tid+1)
name = t.id().split('.')[-1]
doc = t.shortDescription()or""
desc = doc and('%s: %s'%(name, doc))or name
tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
# o and e should be byte string because they are collected from stdout and stderr?ifisinstance(o,str):# TODO: some problem with 'string_escape': it escape \n and mess up formating# uo = unicode(o.encode('string_escape'))# uo = o.decode('latin-1')
uo = e
else:
uo = o
ifisinstance(e,str):# TODO: some problem with 'string_escape': it escape \n and mess up formating# ue = unicode(e.encode('string_escape'))# ue = e.decode('latin-1')
ue = e
else:
ue = e
script = self.REPORT_TEST_OUTPUT_TMPL %dict(id= tid,
output = saxutils.escape(str(uo)+ue),)
row = tmpl %dict(
tid = tid,
Class =(n ==0and'hiddenRow'or'none'),
style = n ==2and'errorCase'or(n ==1and'failCase'or'none'),
desc = desc,
script = script,
status = self.STATUS[n],)
rows.append(row)ifnot has_output:returndef_generate_ending(self):return self.ENDING_TMPL
############################################################################### Facilities for running tests from the command line############################################################################### Note: Reuse unittest.TestProgram to launch test. In the future we may# build our own launcher to support more specific command line# parameters like test title, CSS, etc.classTestProgram(unittest.TestProgram):"""
A variation of the unittest.TestProgram. Please refer to the base
class for command line parameters.
"""defrunTests(self):# Pick HTMLTestRunner as the default test runner.# base class's testRunner parameter is not useful because it means# we have to instantiate HTMLTestRunner before we know self.verbosity.if self.testRunner isNone:
self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
unittest.TestProgram.runTests(self)
main = TestProgram
############################################################################### Executing this module from the command line##############################################################################if __name__ =="__main__":
main(module=None)
import unittest
import HTMLTestRunner
import time
from MatrixWinTest import MatrixWinTest
if __name__ =="__main__":
now = time.strftime("%Y-%m-%d-%H_%M_%S", time.localtime(time.time()))print( now )
testunit = unittest.TestSuite()
testunit.addTest(unittest.makeSuite(MatrixWinTest ))
htmlFile =".\\"+now+"HTMLtemplate.html"print('htmlFile='+ htmlFile)
fp =open(htmlFile,'wb')
runner = HTMLTestRunner.HTMLTestRunner(
stream=fp,
title=u"PyQt5测试报告",
description=u"用例测试情况")
runner.run(testunit)
fp.close()
版权归原作者 Ding Jiaxiong 所有, 如有侵权,请联系我们删除。