PyQt5桌面应用开发(21):界面设计结果自动测试(二)

news2024/11/16 3:35:28

本文目录

  • PyQt5桌面应用系列
  • TDD+UI
    • 为什么?
  • 开发任务
    • 任务设计
    • 小码的工作
      • unittest函数一览表
    • UI单元测试代码
    • 控件代码
    • 测试报告
  • 总结

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桌面应用开发(21):界面设计结果自动测试(二)

TDD+UI

上一篇简单说了下什么是测试驱动开发(TDD)。这在软件开发行业是比较常规的做法,因为只要不是完美主义强迫症的TDD,对于软件开发的质量管理是很有帮助的,因为TDD把软件开发的质量管理(计划-执行-检查-改进)的循环变得更加可用。

但是加上UI后,这个问题就变得微妙起来了。UI是一种与人相关的内容,最好的做法当然是用人作为测试,常规开发过程中,也的确是依靠人力来进行测试。

那么采用TDD来开发UI,是否能行呢?答案是肯定的。软件行业,如果说有什么事情是不可行的,那么就只有说别的事情是不可行这个事情是不可行的。万物皆虚,万事皆允。软件开发的魅力不就在这里吗?

第二个问题就是,值得这么做吗?也有不多的几篇文章探讨了这个问题。也有两篇正经发表的会议文章写了这个事情。但是文章那么少就说明一个问题,可能意义不大,收益不高。

为什么?

有几个因素限制了TDD开发UI。

首先,UI的重要元素,视觉,难以测试,写形式化测试很难,所以这部分意义不大,比如界面布局,pyqt5的设计工具里面可以用C+R来观察布局,这个预览功能中,还能完美支持界面缩放这些操作,比如界面动画,那个测试难度可想而知,现在能考虑的最大可能就是截图比较像素,然后用某种大模型来判定,但是整这个意义不大,来个儿童就能完成;

其次,UI的完整状态空间特别大,如果要写一个全状态转换的状态机,显然是不可能的,因为排列的状态是界面交互元素个数阶乘量级,所以这样做的可行性也值得怀疑。

最后,UI的设计中,如果采用MVVM或者MVC模式,一般会尽量把功能、数据、交互分开,这样各个部分的单元测试才得以进行,界面的综合功能,还是采用综合测试的比较多。

这么分析下来,思路这篇文章就没多少意义了。但是我都已经写到这里,骑虎难下,还得硬撑下去。

那么先确定几个原则:

  1. 有限测试:把TDD作为开发工具,太难的,太麻烦的不搞,不搞完美主义;
  2. 人工视觉:不测试UI的布局、控件的位置、控件的名字、控件文字标签这些一眼就能看到的东西;
  3. 开发规范:应用场景考虑为刚入职的萌新提供较小的控件开发输入。

确定这个几个原则之后,我们来设计一个场景。

开发任务

任务设计

假设公司来一个小码,他各项技能点满,什么都能干,于是安排小码带一个团队编个PyQt5的工控。小码把其中的一个功能抽象为一个控件,这个控件要换算露点和相对湿度。

此处忽略饱和蒸气压、湿空气、理想气体、多组分气体分压、露点、相对湿度的内容15000字。我们最终只需要知道露点是压力、温度和相对湿度的函数,对应的通过压力、温度和露点也能唯一计算相对湿度。

D = f ( p , T , R ) D = f(p, T, R) D=f(p,T,R)

R = g ( p , T , D ) R = g(p, T, D) R=g(p,T,D)

最终有一个调库大牛编写了一个库,这个库需要CoolProp(pip install CoolProp)
来计算湿空气的热力学特性,提供接口全部使用SI单位,这个库提供两个静态方法(类方法)计算上面两个函数。另外这个库还能通过改变 p p p, T T T, D D D中任意一个来自动更新相对湿度,如果改变相对湿度,则自动改变露点。最后,这个库还能输入一个字符串元组表达四个值。这个库设计的不咋地,但是你懂的,热力学那帮人怎么会编程序……

from CoolProp.CoolProp import HAPropsSI


class RelativeHumidityModelSI:
    """
    HumidAir model with SI units
    temperature/dewpoint: K
    pressure: Pa
    """

    def __init__(self):
        self._pressure: float = 1e5
        self._temperature: float = 300.0
        self._dewpoint: float = 273.0
        self._rh: float = self.rh(self._pressure, self._temperature, self._dewpoint)

    @classmethod
    def rh(cls, p, t, d):
        return round(HAPropsSI("R", "P", p, "T", t, "D", d) * 10000) / 100.0

    @classmethod
    def dp(cls, p, t, r):
        return HAPropsSI("D", "P", p, "T", t, "R", r)

    def _update_rh(self):
        self._rh = self.rh(self._pressure, self._temperature, self._dewpoint)

    def _update_dp(self):
        self._dewpoint = round(self.dp(self._pressure, self._temperature, self._rh / 100.0) * 100) / 100.0

    def str_tuple(self):
        return ("{:.2f}".format(i) for i in [self._pressure, self._temperature, self._dewpoint, self._rh])

    @property
    def pressure(self):
        return self._pressure

    @pressure.setter
    def pressure(self, value):
        self._pressure = value
        self._update_rh()

    @property
    def temperature(self):
        return self._temperature

    @temperature.setter
    def temperature(self, value):
        self._temperature = value
        self._update_rh()

    @property
    def dewpoint(self):
        return self._dewpoint

    @dewpoint.setter
    def dewpoint(self, value):
        self._dewpoint = value
        self._update_rh()

    @property
    def relative_humidity(self):
        return self._rh

    @relative_humidity.setter
    def relative_humidity(self, value):
        self._rh = value
        self._update_dp()

小码的工作

小码把编写这个PyQt5组件的任务交给了,比如说,小机。为了让小机好好干活对他进行合理恰当的压榨,引入了单元测试,让小机写一个能通过测试的组件,至于组件是否好看,那就看小机自己的眼睛了……

小码前面已经学习过QTest类的各种方法,能够点击按钮、键盘输入、移动鼠标,就只需要再学习一下unittest的函数:

unittest函数一览表

NameFunction
assertTrue表达式是否为True。
assertFalse检查表达式是否为False。
assertIs等效于assertTrue(a is b)。
assertIsNot等效于assertTrue(a is not b)。
assertIsInstance等效于assertTrue(isinstance(obj, cls))。
assertNotIsInstance与assertIsInstance对应,并非该类的对象。
assertIsNone等效于assertTrue(obj is None)。
assertIsNotNone与assertIsNone对应。
assertEqual运算符’=='意义上的相等。
assertNotEqual两个对象不相等,含义为’!='。
assertGreater等效于assertTrue(a > b)。
assertGreaterEqual等效于assertTrue(a >= b)。
assertLess等效于assertTrue(a < b)。
assertLessEqual等效于assertTrue(a <= b),提示信息更有效。
assertAlmostEqual在一定的允差下的近似相等。
assertNotAlmostEqual与assertAlmostEqual对应,不在允差范围内。
assertIn等效于assertTrue(a in b)。
assertNotIn等价于assertTrue(a not in b)
assertSequenceEqual序列相等(列表或者元组)
assertCountEqual测试集合元素的个数。
assertMultiLineEqual多行字符串相等。
assertTupleEqual元组相等。
assertListEqual列表相等。
assertSetEqual集合相等。
assertDictContainsSubset测试两个字典的包含性,一个是另外一个的子集。
assertDictEqual比较两个字典,assertEqual对字典同样是调用此方法。
assertRegex匹配到正则表达式。
assertNotRegex不匹配正则表达式。
assertLogs日志信息被处罚,按照日志等级来判定。
assertWarns触发特定的 warnClass
assertWarnsRegex警告消息与regexp吻合。
assertRaises触发特定的Exception类。
assertRaisesRegex触发的Exception对应的消息符合正则表达式。
assertNotEquals废弃,用assertNotEqual替代
assertAlmostEquals废弃,用assertAlmostEqual替代
assertEquals废弃,用assertEqual替代
assertNotAlmostEquals废弃,用assertNotAlmostEqual替代
assertNotRegexpMatches废弃,用assertNotRegex替代
assertRaisesRegexp废弃,用assertRaisesRegex替代
assertRegexpMatches废弃,用assertRegex替代
assert_废弃,用assertTrue替代

了解这些之后,小码很快就写出了下面的测试代码:

UI单元测试代码

import unittest
from unittest import TestCase

from HtmlTestRunner import HTMLTestRunner
from PyQt5.QtCore import Qt
from PyQt5.QtTest import QTest
from PyQt5.QtWidgets import QLineEdit, QWidget, QApplication, QComboBox, QPushButton

import relative_humidity_coolprop
from relative_humidity_ui import RelativeHumidityWidget


class RelativeHumidityWidgetConstruction(TestCase):

    def setUp(self) -> None:
        self.app = QApplication([])
        self.widget = RelativeHumidityWidget()
        self.widget.show()
        QTest.qWaitForWindowExposed(self.widget)

    def tearDown(self) -> None:
        self.app.exit(0)

    # four line edit to represent four variables
    def test_interact_components(self):
        self.assertIsInstance(self.widget, QWidget)

        self.assertIsInstance(self.widget.pressureLineEdit, QLineEdit)
        self.assertIsInstance(self.widget.temperatureLineEdit, QLineEdit)
        self.assertIsInstance(self.widget.dewpointLineEdit, QLineEdit)
        self.assertIsInstance(self.widget.relativeHumidityLineEdit, QLineEdit)

        self.assertIsInstance(self.widget.calrh, QPushButton)
        self.assertIsInstance(self.widget.caldp, QPushButton)

        self.assertIsInstance(self.widget.pressureUnit, QComboBox)
        cb: QComboBox = self.widget.pressureUnit
        pressure_units = ['Pa', 'kPa', 'atm', 'bar', 'hPa']
        units = [cb.itemText(i) for i in range(cb.count())]
        self.assertCountEqual(pressure_units, units)
        self.assertListEqual(pressure_units, units)

        self.assertIsInstance(self.widget.temperatureUnit, QComboBox)
        cb: QComboBox = self.widget.temperatureUnit
        temperature_units = ["K", "℃"]
        units = [cb.itemText(i) for i in range(cb.count())]
        self.assertCountEqual(temperature_units, units)
        self.assertListEqual(temperature_units, units)

        self.assertIsInstance(self.widget.dewpointUnit, QComboBox)
        cb: QComboBox = self.widget.dewpointUnit
        temperature_units = ["K", "℃"]
        units = [cb.itemText(i) for i in range(cb.count())]
        self.assertCountEqual(temperature_units, units)
        self.assertListEqual(temperature_units, units)

    def test_initial_values(self):
        self.assertAlmostEqual(1e5, self.widget.pressure, delta=1e-5)
        self.assertAlmostEqual(300.0, self.widget.temperature, delta=1e-5)
        self.assertAlmostEqual(273.0, self.widget.dewpoint, delta=1e-5)
        self.assertAlmostEqual(17.07, self.widget.rh, delta=1e-2)

    def test_value_accessors(self):
        for p, value in zip(
                [self.widget.pressureLineEdit,
                 self.widget.temperatureLineEdit,
                 self.widget.dewpointLineEdit,
                 self.widget.relativeHumidityLineEdit],
                [lambda: self.widget.pressure,
                 lambda: self.widget.temperature,
                 lambda: self.widget.dewpoint,
                 lambda: self.widget.rh]):
            p: QLineEdit
            p.setText("akjdlajf")
            self.assertRaises(ValueError, lambda: value())
            p.setText("1e6")
            self.assertAlmostEqual(1e6, value(), delta=1e-6)

    def test_cal_relative_humidity(self):
        """
        Test calrh button
        :return:
        """
        # setup line edits for pressure, temperature, and dewpoint
        p: QLineEdit = self.widget.pressureLineEdit
        t: QLineEdit = self.widget.temperatureLineEdit
        d: QLineEdit = self.widget.dewpointLineEdit
        p.setText("1e5")
        t.setText("300")
        d.setText("250")
        # click calrh button
        QTest.mouseClick(self.widget.calrh, Qt.LeftButton)
        rh_expected = relative_humidity_coolprop.RelativeHumidityModelSI.rh(1e5, 300, 250)
        # check relative humidity
        self.assertAlmostEqual(rh_expected, self.widget.rh, delta=1e-2)

    def test_cal_dew_point(self):
        """
        Test caldp button
        :return:
        """
        # setup line edits for pressure, temperature, and relative humidity
        p: QLineEdit = self.widget.pressureLineEdit
        t: QLineEdit = self.widget.temperatureLineEdit
        rh: QLineEdit = self.widget.relativeHumidityLineEdit
        p.setText("1e5")
        t.setText("300")
        rh.setText("50")
        # click caldp button
        QTest.mouseClick(self.widget.caldp, Qt.LeftButton)
        dp_expected = relative_humidity_coolprop.RelativeHumidityModelSI.dp(1e5, 300, 0.5)
        # check dew point
        self.assertAlmostEqual(dp_expected, self.widget.dewpoint, delta=1e-2)

        # set unit to degree C
        self.widget.dewpointUnit.setCurrentText("℃")
        QTest.mouseClick(self.widget.caldp, Qt.LeftButton)
        dp_expected = relative_humidity_coolprop.RelativeHumidityModelSI.dp(1e5, 300, 0.5)
        # check dew point return with SI
        self.assertAlmostEqual(dp_expected, self.widget.dewpoint, delta=1e-2)
        self.assertAlmostEqual(dp_expected - 273, float(self.widget.dewpointLineEdit.text()), delta=1e-2)

    def test_change_pressure_unit(self):
        self.assertAlmostEqual(1e5, self.widget.pressure, delta=1e-5)
        self.widget.pressureUnit.setCurrentText("kPa")
        self.assertAlmostEqual(1e5 * 1000, self.widget.pressure, delta=1e-5)
        self.widget.pressureUnit.setCurrentText("atm")
        self.assertAlmostEqual(1e5 * 101325, self.widget.pressure, delta=1e-5)
        self.widget.pressureUnit.setCurrentText("bar")
        self.assertAlmostEqual(1e5 * 1e5, self.widget.pressure, delta=1e-5)
        self.widget.pressureUnit.setCurrentText("hPa")
        self.assertAlmostEqual(1e5 * 1e2, self.widget.pressure, delta=1e-5)
        self.widget.pressureUnit.setCurrentText("Pa")
        self.assertAlmostEqual(1e5, self.widget.pressure, delta=1e-5)

    def test_change_temperature_unit(self):
        self.assertAlmostEqual(300, self.widget.temperature, delta=1e-5)
        self.widget.temperatureUnit.setCurrentText("℃")
        self.assertAlmostEqual(300 + 273, self.widget.temperature, delta=1e-5)
        self.widget.temperatureUnit.setCurrentText("K")
        self.assertAlmostEqual(300, self.widget.temperature, delta=1e-5)

    def test_change_dewpoint_unit(self):
        self.assertAlmostEqual(273, self.widget.dewpoint, delta=1e-5)
        self.widget.dewpointUnit.setCurrentText("℃")
        self.assertAlmostEqual(273 + 273, self.widget.dewpoint, delta=1e-5)
        self.widget.dewpointUnit.setCurrentText("K")
        self.assertAlmostEqual(273, self.widget.dewpoint, delta=1e-5)


if __name__ == '__main__':
    # unittest.main()
    suite = unittest.TestLoader().loadTestsFromTestCase(RelativeHumidityWidgetConstruction)
    runner = HTMLTestRunner(verbosity=2, output='report', report_name='report', add_timestamp=True,
                            combine_reports=True)
    # unittest.TextTestRunner(verbosity=2).run(suite)
    runner.run(suite)

这里为了方便小机编写的空间在其他成立使用,首先小码需要规定那些这个控件是QWidget的子类,还必须提供一些接口,比如获得压力、温度、露点、相对湿度(全部采用SI单位),另外,为了使用方便,还要规定提供表达单位的空间,必须是QComboBox,提供几种单位(压力和温度单位),最后要求设置的数值不合理的时候,要触发ValueError(这个实在太牵强……因为小码也是个渣渣),最后就是改变一个量,对应的相对湿度(或者露点)的变化。

写完这个,小码算是松了一口气。压力给到了小机这边。

控件代码

小机用尽了洪荒之力,反复徘徊于单元测试错误的重压之下,最终提交了一个代码:

import sys

from PyQt5 import uic
from PyQt5.QtWidgets import QWidget, QLineEdit, QApplication, QComboBox, QPushButton

from relative_humidity_coolprop import RelativeHumidityModelSI


def temperature_SI(val, unit):
    """
    :param val:
    :param unit:  K, degC
    :return: val in K
    """
    if unit == "K":
        return val
    if unit == "℃":
        return val + 273
    raise ValueError(f"{unit} should be K or ℃")


def pressure_SI(val, unit):
    if unit == "Pa":
        return val
    if unit == "kPa":
        return val * 1e3
    if unit == "atm":
        return val * 101325
    if unit == "bar":
        return val * 1e5
    if unit == "hPa":
        return val * 100
    raise ValueError(f"{unit} should be Pa, kPa, atm, bar, or hPa")


class RelativeHumidityWidget(QWidget):
    def __init__(self, parent=None):
        self.model = RelativeHumidityModelSI()
        super(RelativeHumidityWidget, self).__init__(parent)
        uic.loadUi("relative_humidity.ui", self)

        self.pressureLineEdit: QLineEdit
        self.pressureUnit: QComboBox
        self.temperatureLineEdit: QLineEdit
        self.temperatureUnit: QComboBox
        self.dewpointLineEdit: QLineEdit
        self.dewpointUnit: QComboBox
        self.relativeHumidityLineEdit: QLineEdit
        self.calrh: QPushButton
        self.caldp: QPushButton

        p, t, d, r = self.model.str_tuple()
        self.pressureLineEdit.setText(p)
        self.temperatureLineEdit.setText(t)
        self.dewpointLineEdit.setText(d)
        self.relativeHumidityLineEdit.setText(r)

        self.calrh.clicked.connect(self.calculate_rh)
        self.caldp.clicked.connect(self.calculate_dp)

    def calculate_dp(self):
        self.model.pressure = self.pressure
        self.model.temperature = self.temperature
        self.model.relative_humidity = self.rh
        p, t, d, r = self.model.str_tuple()
        if self.dewpointUnit.currentText() == "℃":
            d = "{:.2f}".format(self.model.dewpoint - 273)
        self.dewpointLineEdit.setText(d)

    def calculate_rh(self):
        self.model.pressure = self.pressure
        self.model.temperature = self.temperature
        self.model.dewpoint = self.dewpoint
        p, t, d, r = self.model.str_tuple()
        self.relativeHumidityLineEdit.setText(r)

    @property
    def temperature(self):
        value: str = self.temperatureLineEdit.text()
        unit: str = self.temperatureUnit.currentText()
        return temperature_SI(float(value), unit)

    @property
    def pressure(self):
        value: str = self.pressureLineEdit.text()
        return pressure_SI(float(value), self.pressureUnit.currentText())

    @property
    def dewpoint(self):
        value: str = self.dewpointLineEdit.text()
        unit: str = self.dewpointUnit.currentText()
        return temperature_SI(float(value), unit)

    @property
    def rh(self):
        value: str = self.relativeHumidityLineEdit.text()
        return float(value)


if __name__ == '__main__':
    app = QApplication([])
    widget = RelativeHumidityWidget()
    widget.show()

    sys.exit(app.exec_())

界面设计,当然是拖几个控件,搞搞布局就行。

在这里插入图片描述

对应的ui文件也很简单。

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
    <class>Form</class>
    <widget class="QWidget" name="Form">
        <property name="geometry">
            <rect>
                <x>0</x>
                <y>0</y>
                <width>696</width>
                <height>185</height>
            </rect>
        </property>
        <property name="sizePolicy">
            <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
                <horstretch>1</horstretch>
                <verstretch>0</verstretch>
            </sizepolicy>
        </property>
        <property name="windowTitle">
            <string>Form</string>
        </property>
        <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0">
            <item>
                <layout class="QGridLayout" name="gridLayout" columnstretch="0,1,0" rowminimumheight="1,1,1,1,0">
                    <item row="0" column="0">
                        <widget class="QLabel" name="label">
                            <property name="text">
                                <string>Pressure</string>
                            </property>
                        </widget>
                    </item>
                    <item row="2" column="1">
                        <widget class="QLineEdit" name="dewpointLineEdit"/>
                    </item>
                    <item row="0" column="1">
                        <widget class="QLineEdit" name="pressureLineEdit"/>
                    </item>
                    <item row="1" column="1">
                        <widget class="QLineEdit" name="temperatureLineEdit"/>
                    </item>
                    <item row="2" column="2">
                        <widget class="QComboBox" name="dewpointUnit">
                            <item>
                                <property name="text">
                                    <string>K</string>
                                </property>
                            </item>
                            <item>
                                <property name="text">
                                    <string></string>
                                </property>
                            </item>
                        </widget>
                    </item>
                    <item row="3" column="1">
                        <widget class="QLineEdit" name="relativeHumidityLineEdit"/>
                    </item>
                    <item row="0" column="2">
                        <widget class="QComboBox" name="pressureUnit">
                            <item>
                                <property name="text">
                                    <string>Pa</string>
                                </property>
                            </item>
                            <item>
                                <property name="text">
                                    <string>kPa</string>
                                </property>
                            </item>
                            <item>
                                <property name="text">
                                    <string>atm</string>
                                </property>
                            </item>
                            <item>
                                <property name="text">
                                    <string>bar</string>
                                </property>
                            </item>
                            <item>
                                <property name="text">
                                    <string>hPa</string>
                                </property>
                            </item>
                        </widget>
                    </item>
                    <item row="2" column="0">
                        <widget class="QLabel" name="label_5">
                            <property name="text">
                                <string>Dew Point</string>
                            </property>
                        </widget>
                    </item>
                    <item row="1" column="0">
                        <widget class="QLabel" name="label_2">
                            <property name="text">
                                <string>Temperature</string>
                            </property>
                        </widget>
                    </item>
                    <item row="1" column="2">
                        <widget class="QComboBox" name="temperatureUnit">
                            <item>
                                <property name="text">
                                    <string>K</string>
                                </property>
                            </item>
                            <item>
                                <property name="text">
                                    <string></string>
                                </property>
                            </item>
                        </widget>
                    </item>
                    <item row="3" column="0">
                        <widget class="QLabel" name="label_7">
                            <property name="text">
                                <string>RH</string>
                            </property>
                        </widget>
                    </item>
                    <item row="3" column="2">
                        <widget class="QLabel" name="label_3">
                            <property name="text">
                                <string>%</string>
                            </property>
                        </widget>
                    </item>
                </layout>
            </item>
            <item>
                <layout class="QHBoxLayout" name="horizontalLayout">
                    <item>
                        <widget class="QPushButton" name="calrh">
                            <property name="text">
                                <string>Relative Humidity</string>
                            </property>
                        </widget>
                    </item>
                    <item>
                        <widget class="QPushButton" name="caldp">
                            <property name="text">
                                <string>Dew Point</string>
                            </property>
                        </widget>
                    </item>
                </layout>
            </item>
            <item>
                <spacer name="verticalSpacer">
                    <property name="orientation">
                        <enum>Qt::Vertical</enum>
                    </property>
                    <property name="sizeType">
                        <enum>QSizePolicy::MinimumExpanding</enum>
                    </property>
                    <property name="sizeHint" stdset="0">
                        <size>
                            <width>0</width>
                            <height>0</height>
                        </size>
                    </property>
                </spacer>
            </item>
        </layout>
    </widget>
    <resources/>
    <connections/>
</ui>

测试报告

最终就能够运行python relative_humidity_unittest.py得到测试报告:

在这里插入图片描述

总结

通过完整走一遍TDD驱动的UI开发,小码和小机有几个体会。

  1. TDD对于UI设计有至少5毛钱的作用,通过编写单元测试,对单一控件的设计概念到设计实现由很好的推动作用;
  2. 看起来不是很好处理设计变更的……因为测试算例多了之后,跟踪变化将会消耗大量的注意力;
  3. 有单元测试的UI开发确实挺轻松,这样设计的工作就被很好地分离为功能设计和功能实现,对于某些场景的软件开发组织应该有很高的价值;
  4. TDD驱动UI设计,真的要注意的是,界面的正式设计文档、UI/UX设计的结果与测试用例之间的一致性问题。
  5. 行为测试的算例编写实际上进行的是设计工作,这一点必须要弄很清楚,所以编写UI测试用例不应该有一些一蹴而就、三两下就行的奢望,这是设计工作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/655695.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python基础(2)——Python解释器

Python基础&#xff08;2&#xff09;——Python解释器 文章目录 Python基础&#xff08;2&#xff09;——Python解释器目标一. 解释器的作用二. 下载Python解释器三. 安装Python解释器总结 目标 解释器的作用下载Python解释器安装Python解释器 一. 解释器的作用 Python解释…

Golang每日一练(leetDay0099) 单词规律I\II Word Pattern

目录 290. 单词规律 Word Pattern &#x1f31f;  291. 单词规律 II Word Pattern ii &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rust每日一练 专栏 Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 …

ubuntu 20.04 arm 平台交叉编译 glib 库

glib 是什么&#xff1f; glib 是一个比较强大的 软件库&#xff0c;类似于 libc 库 交叉编译 当前需要移植到 arm&#xff08;ARM 32位&#xff09;平台上&#xff0c;需要使用 arm 交叉编译工具链编译 glib 环境准备 ubuntu 20.04 安装较新版本的 meson &#xff1a; &g…

手写-js节流(定时器+时间差两种方式)

官方解释&#xff1a;当持续触发事件时&#xff0c;保证一定时间段内只调用一次事件处理函数。 节流实现思路: 实现节流函数, 我们使用定时器是不方便管理的, 实现节流函数我们采用另一个思路 我们获取一个当前时间nowTime, 我们使用new Date().gettime()方法获取, 在设定一个…

[RPC]:Feign远程调用

文章目录 1 RPC框架-Feign1.1 什么是Feign1.2 Feign解决的问题1.2.1 使用RestTemplate发送远程调用代码1.2.2 存在的问题 1.3 Feign如何使用1.3.1 引入依赖 1 RPC框架-Feign 1.1 什么是Feign Feign是一个简化HTTP客户端编写的框架&#xff0c;通过声明式方式将远程服务调用封装…

基于html+css的图展示131

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

Three.js教程:平行光与环境光

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 其他系列工具&#xff1a; NSDT简石数字孪生 平行光与环境光 本节课通过平行光DirectionalLight (opens new window)和环境光AmbientLight (opens new window)进一步了解光照对应模型Mesh表面的影响。 点光源辅助观察Poin…

管理类联考——英语——技巧篇——新题型——经典方法论

新题型可以说是考研英语独有的考查形式&#xff0c;自2005年起&#xff0c;阅读理解部分便新增了PartB&#xff0c;即新题型。之所以叫作新题型&#xff0c;原因很简单&#xff0c;是因为它在考研英语的各个题型中&#xff0c;年龄是最小的&#xff0c;也就是最新的题型。新题型…

【MySQL表的增删改查】

MySQL表的增删改查 1. CRUD2. 新增&#xff08;Create&#xff09;2.1 单行数据 全列插入2.2 多行数据 指定列插入 3. 查询&#xff08;Retrieve&#xff09;3.1 全列查询3.2 指定列查询3.3 查询字段为表达式3.4 别名3.5 去重&#xff1a;DISTINCT3.6 排序&#xff1a;ORDER …

Hibernate框架【一】——HIbernate框架介绍

系列文章目录 Hibernate框架【三】——基本映射——一对一映射 Hibernate框架【四】——基本映射——多对一和一对多映射 Hibernate框架【五】——基本映射——多对多映射 Hibernate框架介绍 系列文章目录前言一、什么是HIbernate框架Hibernate架构图Hibernate提供的核心功能和…

php-xhprof 学习历程

听说 php-xhprof 可以分析网站的性能。今天闲来无事&#xff0c;就学习了一下。 1、安装&#xff08;万事都得从安装开始&#xff09; #首先&#xff0c;他是需要 graphviz 支持的。所以&#xff0c;我们得先安装这个依赖 yum install graphviz #就只需要这一行命令即可#下载地…

【C++】函数重载及引用

目录 一、函数重载 1、函数重载的概念 2、名字修饰 二、引用 1、引用概念 2、引用特性 3、常引用 4、引用的使用场景 4.1 做参数 4.2 做返回值 5、传值、传引用效率比较 6、引用和指针的区别 一、函数重载 1、函数重载的概念 函数重载&#xff1a;是函数的一种特殊情况&#…

leetcode415. 字符串相加

个人主页&#xff1a;平行线也会相交 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【LeetCode】 &#x1f353;希望我们一起努力、成长&#xff0c;共同进步。 题目链接 给定两个字符串形式的非负整数 num1 和num2 &#…

OpenCV项目开发实战-- 的单应性(Homography)实例Python/C++代码实现

文末附基于Python和C++两种方式实现的测试代码下载链接 什么是单应性(Homography)? 考虑图 1 中所示的平面(书的顶部)的两个图像。红点表示两个图像中的相同物理点。在计算机视觉术语中,我们称这些为对应点。图 1. 显示了四种不同颜色的四个对应点——红色、绿色、黄色和…

【运维知识进阶篇】zabbix5.0稳定版详解1(安装+部署+添加服务器+拆分数据库)

本篇文章介绍zabbix监控&#xff0c;监控是对我们操作系统进行不间断的监控&#xff0c;这是软件生命周期非常重要的一环&#xff0c;可以做到事前告警&#xff0c;事后根据监控内容排查问题&#xff08;金丝雀&#xff0c;监控重要指标&#xff09;&#xff0c;有问题的时候&a…

chatgpt赋能python:Python安装和设置环境变量教程

Python安装和设置环境变量教程 Python是一门非常流行的编程语言&#xff0c;很多开发者都喜欢使用它开发各种应用程序。作为使用Python编程的开发者&#xff0c;你需要安装Python并设置环境变量。在本文中&#xff0c;我们将向你介绍如何安装Python&#xff0c;以及如何设置环…

c++11 标准模板(STL)(std::basic_ios)(二)

定义于头文件 <ios> template< class CharT, class Traits std::char_traits<CharT> > class basic_ios : public std::ios_base 类 std::basic_ios 提供设施&#xff0c;以对拥有 std::basic_streambuf 接口的对象赋予接口。数个 std::basic_ios…

Jenkins pipeline 执行工程目录里的脚本文件

jenkins 工程根目录下有一个脚本文件&#xff0c;如图所示 文件中有一个简单的输出 我们希望在jenkins pipeline 中执行这个脚本文件&#xff0c;如何配置jenkins pipe 命令呢 首先&#xff0c;要明确一点&#xff0c;我们在jenkins pipeline 中&#xff0c;默认是在Jenki…

英语中如何表达各行各业

前言 &#x1f3e0;个人主页&#xff1a;我是沐风晓月 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是沐风晓月&#xff0c;阿里云社区博客专家 &#x1f609;&#x1f609; &#x1f495; 座右铭&#xff1a; 先努力成长自己&#xff0c;再帮助更多的人,一起加油进…

字符串--从函数返回字符串指针

许多字符串处理函数是不需要返回值的&#xff0c;但实际上它们都被设计成了有返回值的函数。例如字符串赋值函数的函数原型为&#xff1a; char *strcpy(char *str1, const char *str2);字符串连接函数strcat()的函数原型为&#xff1b; char *strcat(char *str1,const char …