目录
1.1.5 黑盒测试和白盒测试(按是否查看程序内部代码结构划分)
1.从软件缺陷报告的生命周期上看,测试人员需要对同一个软件模块反复多次执行测试,这叫做回归测试。
2.2.1 Selenium 元素定位及元素操作(包含 Web 表单)
类级别:setUpClass() 与 tearDownClass()
模块级别:setUpModule() 与 tearDownModule()
4、assertIsNone 与 assertIsNotNone
6、assertIsInstance 与 assertNotIsInstance
3.7.3 使用 txt / excel / csv 文件进行参数化
第一阶段:功能测试
1.1 软件测试专业术语
1.1.1 软件测试的概念
GB/T11457对测试的定义:依据规范的软件检测过程和检测方法,按照测试计划和测试需求对被检测软件的文档、程序和数据进行测试的技术活动。
因此,软件测试的工作,不仅是程序测试,还包括数据和文档测试。
1.1.2 软件测试的重要性
第一,软件测试可以减少软件的不正确执行导致的资金、时间和商业信誉损失,甚至能减少人员伤亡风险。
第二,软件测试可以降低软件开发成本,强化项目进度和质量上的控制。
第三,软件测试的发展推动了软件工程的发展。
1.1.3 软件测试的目的
软件测试目的:
- 验证软件是否满足软件开发合同或项目开发计划、系统设计文档、软件需求规格说明、软件设计说明和软件产品说明等规定的软件质量要求;
- 通过测试,发现缺陷;
- 为软件产品的质量测量和评价提供依据。
软件测试目标:
- 确保产品完成了它所承诺或公布的功能(功能性测试)
- 确保产品满足性能和效率的要求(性能测试)
- 确保产品是健壮的、适应用户环境的 (健壮性测试)
1.1.4 软件测试和SQA的区别
软件测试是软件质量保证的一部分
1.1.5 黑盒测试和白盒测试(按是否查看程序内部代码结构划分)
黑盒测试:黑盒测试是把软件产品当作是一个黑盒子,在不考虑程序内部结构的情况下,在程序接口进行测试,它只考虑程序是否能接收输入数据而产生正确的输出结果。
白盒测试:白盒测试是一种以理解软件内部结构运行方式为基础的软件测试技术。测试人员通常需要跟踪一个输入在程序中经过了哪些函数的处理,这些处理方式是否正确。
1.1.6 手工测试和自动化测试(按测试的执行方式划分)
手工测试:是利用人工的方式去执行测试
自动化测试:是利用工具或程序来代替人工的测试方法。
1.1.7 V模型的测试级别
单元测试
单元测试是编写一小段代码,用于检验被测代码的一个很小很明确的功能是否正确,编写的这一小段代码被称为桩或驱动。
集成测试
集成测试是将通过测试的单元模块组装成系统或子系统再进行测试,目的是对组件之间的接口进行测试,以及测试一个系统内不同部分的相互作用。
系统测试
系统测试是将整个软件系统全部集成好之后作为一个整体进行测试,以验证软件系统的正确性和性能是否满足规约所指定的要求。
验收测试
根据用户需求、业务流程进行的正式测试以确保系统符合所有验收准则。
验收测试有下面几种典型的类型:
- 用户验收测试
- 运行(验收)测试
- 合同和法规性验收测试
- α 测试和 β 测试
1.1.8 功能和非功能测试 (按测试目标的不同划分)
功能测试:功能测试就是指验证系统能做什么。
非功能测试:非功能性测试就是测试系统工作的怎样。性能测试、负载测试、压力测试、可用性测试、可维护性测试、可靠性测试、可移植性测试、兼容性测试、安全性测试、本地化测试和配置测试。
1.1.9 静态和动态测试(按是否需要运行程序划分)
静态测试:是指不运行被测软件,只是静态地检查程序代码、界面或文档可能存在的错误的过程方法。
动态测试:是指实际运行被测程序,输入相应的测试数据,检查输出结果和预期结果是否相符的过程。
1.1.10 其他测试术语
确认测试:重新执行上传失败的测试用例,以验证是否已经修复。
回归测试:对软件进行修改之后进行的测试,目的是检验对软件进行的修改是否正确。
冒烟测试:对一个新版本进行大规模的测试之前,先验证一下软件的基本功能是否实现。
随机测试:通常是指临时准备的、随机的缺陷搜索的测试过程。它是让测试人员发挥自己想象的去测试,其目的是模拟用户的真实操作发现一些边缘性的缺陷。
1.2软件测试流程
1.2.1 软件测试需求分析
软件需求分析是软件测试流程中的基础一环,用来明确软件测试对象以及测试范围,并作为测试覆盖的基础。
1.2.2 软件测试计划制订
1.2.3 软件测试用例设计
软件测试用例是指导软件测试工作的一种文档,它是通过使用在软件测试计划中确定的测试技术,对于已确定的软件测试需求进行逐步推敲而设计出来的。
1.2.4 软件测试环境搭建
软件测试环境是指为了完成软件测试工作所必须的计算机硬件、软件、网络设备、历史数据的总称。测试环境要与开发环境分开。
1.2.5 测试数据准备
传统的创建测试数据的方法分为手动创建和自动化创建两种方法。
手动创建测试数据:
- 手动模拟用户的实际操作来创建重要业务流程的测试数据;
- 通过 SQL 语句中 where 查询条件和 update 方法来创建符合条件的测试数据;
- 导入本地机器上存储的一些符合条件的测试数据;
- 导入并加工线上数据变成测试数据。
自动化创建测试数据:
- 使用自动化工具创建:Data Generator、Databene Benerator、Testgen、Datatect、Turbo Data;
- 这几个工具可以为所有类型带的输入域和边界条件生成测试数据,可以生成普通文件或直接向数据库表插入不同类型的数据,比如名字、地址等;
- 使用自建脚本创建:Ruby、Python、Fit、FitNesse、Shell。
1.2.6 测试执行及缺陷处理
测试执行是指按照之前设计好的测试用例,按照里面的步骤一步一步的执行,查看预期结果与实际结果是否一致的过程。
1.2.7 测试总结报告和测试文档归档
测试总结报告
软件测试进行到一定程度就要进行测试评估了。通过测试评估生成的软件测试报告来确定测试是否达到了出口准则。
测试文档归档
常用的文档管理工具: Ftp、SVN、Git、VSS、Wiki 等。
1.3 设计测试用例(前面了解一遍即可,比赛重点如下)
1.3.1 测试用例的概念和作用
测试用例的概念:
软件测试用例是指对一项特定的软件产品进行测试任务的描述,体现测试方案、方法、技术和策略,内容包括测试目标、测试环境、输入数据、测试步骤、预期结果、测试脚本等,并形成文档。
测试用例是指定输入,预期结果和一组测试项的执行条件的文档。
测试用例的作用:
- 避免盲目测试,提高测试效率
- 确保功能需求不被遗漏
- 便于回归测试
- 为测试的度量提供评估基准
1.3.2 获取需求的测试点
测试用例的步骤:
- 获取需求的测试点
- 设计测试用例模板,设计测试步骤
- 确定测试数据
- 评审测试用例
获取需求的测试点:
测试点来源于:显性需求(需求文档)、隐形需求。
- 阅读遗留文档,收集整理已有的需求
- 向相关人员咨询
- 参考同类产品的需求说明
- 采用探索性测试的解决方案
1.3.3 测试用例模板
1.3.4 测试用例的优先级
指测试用例被执行的优先顺序
1.3.5 测试用例的设计原则
- 测试用例的描述要明确
- 测试用例的描述要简洁
- 测试用例对需求的覆盖采用最小化原则
- 测试用例编写要有条理、逻辑性强
- 功能覆盖全面、深入,能够发现软件中更多的缺陷
1.3.6 测试用例的维护
在测试过程中,测试用例并不是一成不变的,它需要不断的更新和维护,这是一个不断修改完善的过程。
1.3.7 【等价类划分法概述】
1、等价类划分法,它将不能穷举的测试过程进行区域划分,减少测试的数量,从而使测试过程合理化。
2、基本思路:把程序的输入域划分成若干个部分,列出哪些数据是有效的,哪些数据是无效的,从每个部分中选取少数代表性数据作为测试用例的数据。这样,每一类的代表数据在测试中的作用都等价于这类中的其他值。
软件不能只接收合理有效的数据,也要具有处理异常数据的功能,这样测试才能确保软件具有更高的可靠性。因此,在等价类划分的过程中,不但要考虑有效等价类,也要考虑无效等价类。
- 有效等价类:是指对软件规格说明来说,合理、有意义的输入数据等构成的集合,利用有效等价类可以检验程序是否满足需求规格说明书所规定的功能和性能。只考虑有效等价类的测试称为“标准等价类测试”。
- 无效等价类:是指不满足程序输入要求或者无效的输入数据所构成的集合,利用无效等价类可以检验程序异常情况的处理。不止考虑了“有效等价类”,还考虑了“无效等价类”的测试被称为“健壮性等价类测试”。
3、划分原则
- 如果程序规定了输入域的取值范围,则可以确定一个有效等价类和 2 个无效等价类。
- 如果程序规定了输入值的集合,不是一个范围,则可以确定一个有效等价类和一个无效等价类。
- 如果程序规定了输入数据的一组值,并且程序要对每一个输入值分别进行处理,则可以每一个值确定一个有效等价类,然后再选择一个无效等价类。
- 如果程序规定了输入数据必须遵守的规则,则可以确定一个有效等价类和若干个无效等价类。
- 如果已知的等价类中各个元素在程序中的处理方式不同,则应将该等价类进一步划分成更小的等价类。
4、使用等价类划分法设计测试用例的步骤如下:
第 1 步:分析程序的规格说明,列出有效等价类和无效等价类;列出等价类表,并对每个等价类规定唯一的编号,如下表所示。当然也可以不是表格形式,采用文字描述的形式。
第 2 步:一一列出输入条件中可能的组合输入情况。
第 3 步:选取合适的数据,编写测试用例
1.3.8 等价类-注册界面案例
1.3.9 等价类-判断三角形形状案例
1.3.10 等价类划分法小结
- 在等价类划分法中,每一类的代表性数据(也就是被选为测试用例的测试数据),在测试中的作用等价于这一类中的其他值
- 也就是说如果等价类中的一个测试数据能捕获一个缺陷,那么该等价类中的其他测试数据也能捕获该缺陷;如果等价类中的一个测试数据不能捕获缺陷,那么选择该等价类中的其他测试数据也不能捕获缺陷。
- 只要有数据输入的地方,就可以采用等价类划分法,它可以从无限多的数据中选取少数代表性的数据进行测试以减少测试人员的工作量。
- 注意:避免“屏蔽”现象发生(无效等价类在开始测试的时候不能一起组合)
1.3.11 边界值划分法
1、上点、离点、内点的确定。
1.3.12 判定表法概述
1、边界值分析法和等价类划分法,没有对输入条件的组合进行分析,对多个条件进行的组合测试的时候,我们就需要采用判定表法。
2、判定表又称“决策表”,是一种表格状的图形工具,适用于处理判断条件较多,各条件又相互组合、有多种决策方案的情况。
3、判定表通常由 4 个部分组成:
- 条件桩:指所有条件的名称,列出的条件的先后次序无关紧要。
- 动作桩:指所有可能采取的操作,顺序没有约束。
- 条件项:条件桩中的条件所有可能的取值。
- 动作项:与条件项紧密相关,列出在条件项的各组取值情况下应该采取的动作。
任何一个条件组合的特定取值及其相应要执行的操作称为一条规则,在判定表中贯穿条件项和动作项的一列就是一条规则。显然,判定表中条件有多少组合取值,也就有多少条规则
4、建立判定表的步骤如下:
- 第 1 步:分析需求,列出所有的条件桩和条件项;
- 第 2 步:分析需求,列出所有的动作桩和动作项;
- 第 3 步:根据规则,设计初始判定表;
- 第 4 步:简化判定表,合并相似规则,设计测试用例。
1.3.13 判定表法案例-超市货架
1.3.14 判定表法案例-房屋销售
1.3.15 判定表法小结
适合使用判定表设计测试用例需要的条件如下:
- 规则说明以判定表的形式给出,或很容易转换成判定表。
- 条件的排列顺序不影响所要执行的操作。
- 规则的排列顺序不影响所要执行的操作。
- 如果某一个规则要执行多个操作,这些操作的执行顺序也无关紧要。
- 当某个规则的条件已经满足,并确定要执行的操作后,不必检验别的规则。
1.3.16 因果图法概述
- 等价类、边界值:着重考虑的输入/输出条件的取值。
- 判定表法:考虑的是输入条件的各种组合情况,但是没有考虑到各个输入之间和输出之间的相互制约关系
- 如果考虑输入条件之间的制约关系,就要使用到因果图法。
1、因果图法基本思路
- 因果图法是一种利用图解法分析输入的各种组合情况,从而设计测试用例的方法,它适合于检查程序输入条件的各种情况的组合。
- 在因果图法中,输入就是因,输出就是果,因之间有相互制约关系,因果之间也有制约关系。
2、关系符合(原因与结果之间的关系)
- 恒等:若 c1是 1,则 e1也是 1;否则 e1为 0;
- 非:若 c1是 1,则 e1是 0;否则 e1 是 1;
- 或:若 c1 或 c2 或 c3 中有一个是 1,则 e1是 1,;否则 e1为 0;
- 与:若 c1 和 c2 以及 c3 都是 1,则 e1是 1;否则 e1为 0。
3、约束符合(原因与原因之间的关系)
- E 约束(异、互斥):a、b、c 中最多有一个可能为 1,也就是 a、b、b 不能同时为 1,输入条件之间为互斥关系。但可以同时为 0。
- I 约束(或、包含):a、b、c 中最少有一个必须是 1,也就是 a、b、c 不能同时为 0,输入条件之间为包含关系。但可以同时为 1。比如程序中的多选按钮。
- O 约束(唯一):a、b、c 中必须有一个且仅有一个为 1。比如程序中的单选按钮。
- R 约束(要求):a 是 1 时,b 必须是 1,a 为 0 时,b 的值不确定。即不可能 a 是 1 时,b 是 0。
- 以上 4 种是输入条件的约束,输出条件的约束只有一种,就是 M 约束:
- M 约束(强制、屏蔽):若 a 是 1,则 b 强制为 0;若 a 是 0,那么 b 的值不确定。
4、使用因果图设计测试用例的步骤如下 :
- 第 1 步:分析待测系统的规格说明,找出输入(原因)与输出(结果)。
- 第 2 步:明确所有原因和结果之间的制约关系以及组合关系,画出因果图。
- 第 3 步:在因果图上标记约束条件。
- 第 4 步:跟踪因果图中的状态条件,把因果图转换为判定表。
- 第 5 步:将判定表中的每一列作为依据,生成测试用例。
1.3.17 因果图法案例-用户名输入框
1.3.18 因果图法小结
1、特点
- 因果图主要考虑条件的取值之间(控件之间)的组合关系和制约关系。
- 每个条件(控件)的取值不宜过多,最好为 2 个或 3 个,比如按钮点击或者不点击,单选按钮选择或者不选择。如果控件较多,或者每个控件的取值较多,组合量将会很大,不宜使用因果图法。
2、缺点
- 输入条件与输出结果的因果关系,有时难以从软件需求规格说明书中得到。
- 有时即使得到了这些因果关系,也会因为因果图关系复杂导致图非常庞大,难以理解,测试用例数目也会极其庞大。
- 熟练之后,可以直接填写判定表,然后编写测试用例,因果图可以省略。
1.3.19 【正交实验法概述】
- 判定表法和因果图法均是考虑有多个输入条件,并且不同的输入条件的组合会得出不同的动作的情况,但他们不适合输入条件过多的情况。
- 正交实验法是一种基于正交表的、高效率、快速、经济的实验设计方法,它研究的“多因素多水平”的情况,然后套用正交表来随机地产生用例(用例之间没有主次之分),是一种提高测试覆盖率的简单易用的方法。
- 因素:在一项实验中,凡是被考查的变量就称为因素。
- 水平:在实验范围内,因素被考查的值称为水平。
- 正交表达公式
L 行数(水平数^因素数)
行数(Runs):正交表中行的个数,也就是实验的次数,也指测试用例的个数。
水平数(Levels):任何单个因素能够取得的值的最大个数。
因素数(Factors):指正交表中列的个数。
查询正交表:通过链接查询:http://support.sas.com/techsup/technote/ts723_Designs.txt
- 正交法是正交性
整齐可比性: 在同一张正交表中,每列(因素)中不同数字出现的次数是完全相同的。
均衡分散性: 在同一张正交表中,任意两列(两个因素)的水平搭配(横向形成的数字对)是完全相同的。
- 正交实验法设计测试用例的步骤:
- 确定表中的因素数。
- 确定每个因素的水平数。
- 选择合适的正交表。
- 把变量的值映射到表中。
- 加上认为可疑且没有在表中出现的组合。
- 设计测试用例。
1.3.20 正交实验法案例-属性设置
1.3.21 正交实验法小结
- 正交实验法能够使用最小的测试过程获得最大的测试覆盖率。
- 目前常见的正交表数量有限
- 学习一种均匀选取测试数据的测试思想
1.3.22 【场景法概述】
- 场景法就是模拟用户操作软件时的场景,主要用于测试系统的业务流程。软件几乎都是由事件触发来控制流程的,事件触发时的情景便形成了场景,而同一事件不同的触发顺序和处理结果就形成事件流。
- 场景法一般包含基本流和备选流。
基本流也叫有效流或正确流,主要是模拟正确的业务操作过程的情景。基本流一般采用直黑线表示,是经过用例的最简单路径,无任何差错,程序从开始直接执行到结束
备选流也叫无效流或错误流,主要是模拟错误的业务操作过程的情景。备选流则采用不同颜色表示。一个备选流可能从基本流开始,在某个特定条件下执行,然后重新加入基本流中,也可以起源于另一个备选流,或终止用例,不再加入到基本流中,备选流一般代表了各种错误情况。
- 识别出基本流和备选流的原则 :
- 一个业务只存在一个基本流;
- 基本流只有一个起点,一个终点;
- 基本流是主流,备选流是支流;
- 备选流可以起始于基本流,也可以起始于其它的备选流;
- 备选流的终点,可以是一个流程出口,也可以是回到基本流,还可以是汇入其它的备选流;
- 如果在流程图中出现了两个不相上下的基本流,一般需要把它们分开对待。
- 应用场景法的基本设计步骤如下:
- 根据需求说明,分析出程序的基本流以及各项备选流;
- 根据基本流和各项备选流生成不同的场景;
- 对每一个场景生成相应的测试矩阵;
- 生成测试用例,去掉多余的测试用例,并确定测试数据值。
1.3.23 场景法案例1-图书管理系统
1.3.24 场景法案例2- 登录流程
1.3.25 场景法小结
- 当程序界面上没有太多填写项,主要通过鼠标的点击、双击、拖拽等完成操作的时候可以使用场景法。
- 使用场景法时,可以把自己当作最终的用户,分析在使用该软件的时候可能遇到的场景,主要是验证业务流程、主要功能的正确性和异常处理能力。
1.3.26 大纲法
- 大纲法是一种着眼于需求功能的方法,是从宏观上检验需求的完成度。汇集了需求文档的核心内容,大纲的每项都可以根据测试人员的喜好以逻辑形式分组。通常我们会将需求转换为大纲树的形式。
- 大纲树是一个迭代的过程,一开始是从需求中产生的,如果需求中没有功能大纲,那么测试人员需要自行列出,并用需求编号实现需求的可跟踪性。并通过与开发人员和 PM 的沟通讨论,不断细化扩大大纲,解决需求中不够明确的问题,大纲的每个后续版本都是对前者的细化。
- 通过列大纲的方式,检测未被覆盖的功能点,从而发现系统的缺陷。是组织思维的方式。
1.3.27 错误推测法
- 基于经验和直觉推测程序中所有可能存在的各种错误,有针对性地设计测试用例的方法。也就是列举出程序中所有可能有的错误和容易发生错误的特殊情况,根据这些情况选择测试用例。
- 它是一项依赖于直觉的非正规的过程,所以无法描述出它的设计步骤。但它的基本思想是列举出可能犯的错误或错误易发情况的清单,然后编写测试用例。
1.3.28 测试用例方法的综合使用
测试用例逐级生成的策略是指:
- 在业务表述层,使用大纲法,对业务表述和功能模块进行整体宏观把控;
- 在剧本层使用场景法来验证业务的触发场景;
- 在对象模型层利用判定表、因果图、正交实验法来测试各种输入控制的输入条件的组合情况;
- 在抽象数据层利用边界值、等价类划分法对具体控件进行测边界测试;
- 然后生成详细的测试用例(如果是自动化测试的话,还要生成测试脚本),再使用错误推测法弥补测试用例没有覆盖到的部分。
1.3.29 测试用例的粒度
简化测试用例----只写测试点
1.4编写缺陷报告
1.4.1 软件缺陷的定义
- 软件未达到产品说明书标明的功能。
- 软件出现了产品说明书指明不会出现的错误。
- 软件功能超出了产品说明书指明的范围。
- 软件未达到产品说明书虽未指出但应达到的目标。
- 软件测试人员认为软件难以理解、不宜使用、运行速度缓慢,或者最终用户认为不好。
除了参考以上 5 个规则之外,还要看使用对象和使用环境
1.4.2 软件缺陷产生的原因和缺陷的识别
缺陷产生的原因:
- 软件缺陷的第一大来源是产品说明书。
- 缺陷的第二大来源是设计。
- 第三大来源才是程序代码。
- 第四大来源是其他的一些原因。
缺陷的识别 :
- 首先,可以把参考文档作为识别和判断缺陷的辅助工具。如软件需求说明书、设计文档、用户手册及联机帮助等,这些文档反映了大量的用户需求,所以被大多数测试人员在实际测试过程中广泛地使用。
- 其次,通过对软件产品的行业知识和行业标准的了解来发现被隐藏的问题,这些问题中往往隐藏着致命缺陷。
- 最后,通过沟通的方式来收集、学习和分享其他人判断缺陷的方法和经验,沟通对象比如项目经理、测试组其他成员、客户、开发人员等。
1.4.3 软件缺陷报告模板
1.4.4 缺陷报告的基本信息
- 缺陷标题(或者叫缺陷摘要,Summary)
- 操作步骤(也叫复现步骤,Reproducible Steps)
- 预期结果(Expected Result)
- 实际结果(Actual Result)
- 注释(Notes)
1.4.5 缺陷报告的属性
- 模块名称(Module)
缺陷发生的功能模块。
- 缺陷版本号(Version)
版本号通常用数字表示,如V1.1等。
- 缺陷的状态(Status)
- 缺陷类型(Type)
- 功能问题(Function
- 接口问题(Interface)
- 逻辑问题(Logic)
- 计算问题(Computation)
- 数据问题(Assignment)
- 用户界面问题(User Interface)
- 文档问题(Documentation)
- 性能问题(Performance)
- 配置问题(Build、Package、Merge)
- 标准问题(Norms)
- 环境问题(Environments)
- 兼容问题(Compatibility)
- 其他问题(Others)
- 缺陷严重等级(Severity)
1-致命缺陷(Fatal)
致命缺陷是指系统任何一个主要功能完全丧失,用户数据受到破坏,系统崩溃、悬挂、死机或者危及人身安全的缺陷。或者系统所提供的功能或服务受到明显的限制,不能执行正常工作流程或实现重要功能
2-严重缺陷(Critical)
指可能导致系统不稳定,运行时好时坏,严重影响系统要求或基本功能实现的缺陷。
3-重要缺陷(Major)
指系统的次要功能没有完全实现,但不影响用户的正常使用,不会影响系统稳定性的缺陷。记录。
4-一般缺陷(Minor)
是指使操作者不方便或遇到麻烦,但它不影响功能的操作和执行,如个别不影响产品理解的错别字、文字排列不整齐等一些小问题,重点指系统的 UI 问题,
5-改进意见(Enhancement)
是系统中值得改良的问题。比如容易给用户错误和歧义的提示;界面需要改进的;某个控件没有对齐等;测试人员可以对有疑虑的部分,提出修改建议。
- 缺陷处理优先级(Priority)
- 缺陷来源
1.4.6 缺陷报告描述的案例
1.4.7 缺陷报告书写的原则
- 遵循 5C 准则,如下图所示
- Correct(准确):每个组成部分的描述准确,不会引起误解和歧义,不夸大缺陷,也不要过于轻描淡写;
- Clear(清晰):每个组成部分的描述清晰,不使用模棱两可的描述,比如出现“似乎(seem)”、“看上去可能(Possible)”等含义模糊的词汇;
- Concise(简洁):只包含必不可少的信息,不包括任何多余的内容。这可以通过使用关键词,使摘要的描述短小简练,又能准确解释产生缺陷的现象。如“在新建任务窗口中,选择直接下达,负责人收不到即时消息”中“新建任务窗口”、“直接下达”、“及时消息”等就是关键词;
- Complete(完整):包含复现该缺陷的完整步骤和其他本质信息,可以使开发人员很容易看懂缺陷;
- Consistent(一致):按照一致的格式书写全部缺陷报告。
- 报告随机缺陷
- 首先,一定要及时详细的记录缺陷并提交到缺陷管理工具中,并在报告此类 Bug 时,明确说明自己不能复现这个程序错误,必要的时候要保存截图和相关日志,为开发解决 Bug 提供思维方向,并适当降低处理优先级。
- 其次,在系统中留下随机缺陷的记录之后,考虑到测试项目的整体进度,对于一时难以再现的缺陷可以暂时搁置,稍后再寻找合适的时间去尽量复现,或者等开发人员有空的时候再一起调试。
- 最后,对随机缺陷要持续关注 3 到 5 个版本,如果在此期间从再未出现过,可以暂时关闭该缺陷,可能程序员在修改别的缺陷的时候无意中修复了这个缺陷;如果随机缺陷再次出现,可以让开发过来测试机前面现场分析。
- 及时报告缺陷
- 小缺陷也值得报告
- 一个缺陷一个报告
- 以中性的语言描述缺陷
- 引用别人的缺陷报告,不要擅自修改
1.4.8 缺陷报告的处理流程
缺陷报告的生命周期
1.新提交的缺陷(New)
首先由测试人员发现缺陷,并提交到缺陷管理工具中,缺陷状态为 New。
2.新打开的缺陷(Open)
测试经理或者项目经理审核状态为 New 的缺陷,把被确认的缺陷分配给对应的开发人员,状态为 Open。
3.已处理的缺陷(Resolved)
开发人员开始处理属于自己的缺陷报告,处理完毕后状态设置为 Resolved,但并非所有的软件缺陷都会得到修复。
- 已经修复(Fixed)
- 推迟解决(Postponed)
- 无法复现(Unreproduced)
- 重复提交(Duplicate)
- 不是缺陷(Invalid)
1.4.9 回归测试
1.从软件缺陷报告的生命周期上看,测试人员需要对同一个软件模块反复多次执行测试,这叫做回归测试。
2.怎么进行充分有效的回归测试
可以使用“基于风险的测试方法”,它的本质是评估系统不同部分蕴含的风险,考虑风险存在的可能性以及造成的影响,并专注于测试那些最高风险的地方。为了评估风险,必须认识到它有两个截然不同的方面,即可能性和影响。
- 可能性:是指可能出错的机会,不考虑影响程度,仅仅考虑出现问题的机会有多大。
- 影响:是确实出错后会造成的影响程度,不考虑可能性,仅仅考虑出现的问题的情况会有多么的糟糕。
3.如何做好回归测试
- 基于风险的测试方法通过策略性地删减,从而减少了重复的工作量,这在一定程度上缓解了测试人员的思维疲劳
- 还需要在测试的适当阶段引入新的测试人员来补充测试,让新加入的测试人员带来新的灵感。
- 使用自动化测试工具来进行回归测试,不仅快、准,而且可以不厌其烦。
第二阶段:自动化测试
2.1 自动化测试概述
2.1.1 UI自动化测试概述
1、什么是自动化测试
自动化测试是一种使用特殊的测试工具和框架测试软件的方法或者方式,可以最大限度地减少人为干预并提升测试效率与质量。
2、自动化测试生命周期
- 确定测试范围
- 选择合适的自动化工具
- 确定测试计划、测试策略以及测试设计
- 配置测试执行环境
- 测试脚本开发与执行
- 生成测试报告并分析结果
3、自动化测试类型
- Smoke Testing 冒烟测试
- Integration Testing 集成测试
- Regression Testing 回归测试
- Security Testing 安全性测试
- Performance Testing 性能测试
- Acceptance Testing 验收测试
2.1.2 Selenium简介
1、Selenium简介
- Selenium 是一个免费、开源的自动化测试框架,可以对不同浏览器上的 Web 应用程序进行自动化测试。
- 需要注意的是 Selenium 不支持 Desktop 应用程序 Mobile 移动应用程序的测试,如果需要测试这些可以使用 Appium 框架。
2、Selenium优势
- Selenium 测试脚本支持使用 Java、Python、C#、PHP、Ruby、Perl 和.Net 中的任何一种编程语言编写
- Selenium 测试可以在 Windows、Mac 或 Linux 中进行。
- Selenium 测试可以使用以下任何一种浏览器运行:Mozilla Firefox、Internet Explorer、Google Chrome 和 Safari 等。
- Selenium 测试用例可以跟 TestNG、JUnit 或者 PyTest 等测试框架集成,用于管理测试用例和生成报告。
- Selenium 可以非常方便的与 Maven、Jenkins 和 Docker 集成,实现持续测试。
3、Selenium工具集
Selenium 不仅仅是一个单一的工具,而是一个软件集合,每个工具都可以满足不同的测试需求。Selenium 由四个部分组成:
- Selenium IDE
IDE 代表集成开发环境,它是浏览器火狐浏览器的插件。可以实现网页操作步骤的录制和回放,使用此插件可以执行测试逻辑简单的自动化测试,并且支持将脚本导出为 Java、Python、C# 等多种语言。
Selenium IDE 适合简单的逻辑,不适合大型项目,推荐使用编程方式编写自动化测试脚本实现自动化测试。
- Selenium RC
RC(Remote Control)代表远程控制,它允许开发人员使用自己喜欢的语言进行编码。Selenium RC支持多种编程语言,如 Java、C#、Python 等,它把 Selenium RC 的服务器作为代理服务器访问应用,实现自动化测试的目的。
- Selenium WebDriver
WebDriver 自动化并控制 Web 浏览器执行的活动。它通常不依赖于 JavaScript,而是与浏览器通信以控制其操作。WebDriver 支持 Java、C#、Python、Ruby 等编程语言。不同类型的 WebDriver 有:
- FirefoxDriver
- InternetExplorerDriver
- ChromeDriver
- SafariDriver
- OperaDriver
- AndroidDriver
- IPhoneDriver
- HTMLUnitDriver
- Selenium Grid
Selenium Grid 和 Selenium RC 一起被用来在多个浏览器之间执行并行测试。
2.1.3 Python 及 Selenium 环境搭建
15届大赛官方提供了备赛环境(下方链接)并且省赛国赛线下赛点提前部署好了被测系统
链接:https://pan.baidu.com/s/15V83ygXDx_ECqVOuhCkVBw
提取码:khsl
2.1.4 WebDriver 入门
1、Selenium WebDriver(以下简称 Driver)的体系结构包括以下四个主要组件:
- Selenium Client
Selenium Client 可以使用市面上流行的编程语言编写的脚本来驱动浏览器自动化。 Selenium Client 充当 Selenium 和测试脚本之间的翻译器。在特定的编程语言中,Selenium Client 将测试脚本翻译成 Selenese。这使得 Selenium 可以很容易地遵循给定的一组指令。 (Selenese 是 Selenium 中的一组命令,有助于测试 Web 应用程序)
- W3C standards 协议
Selenium 4 使用了 W3C standards 协议进行通信,通过协议传递的请求和响应就不需要编码和解码的 API 了。基于 W3C standards 的更新,任何遵循 W3C standards 的软件都可以与 Selenium 4 集成,而不会出现任何兼容性问题。几乎所有浏览器,如 Chrome、Safari 和 IE,都已经符合 W3C 标准。
- 浏览器驱动程序(Driver)
所有浏览器都有自己的驱动程序,用于控制该浏览器中发生的所有操作。当 Selenium Client 向驱动程序发送操作浏览器步骤,驱动程序就会控制浏览器,以便测试人员可以自动执行 Selenium 测试脚本。
- 浏览器
WebDriver 可以与包括 Mozilla Firefox、Microsoft Edge、Safari 等浏览器进行交互。通过 WebDriver 可以自动执行应用在浏览器上的操作。
2、使用 WebDriver 的方式
- 使用 Driver Management 工具
- 硬编码方式
- 添加到环境变量
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get("https://www.baidu.com")
driver.find_element(By.ID, "kw").send_keys("蓝桥云课")
driver.find_element(By.ID, "su").click()
首先创建了一个 Chrome Driver 对象 driver,接着调用 driver 的 get 方法打开了百度首页;
然后通过 driver 的 find_element 方法通过 ID 方式定位到了首页的输入框,通过 send_keys 方法在输入框中输入了 蓝桥云课
再然后也是通过 find_element 方法使用 ID 方式定位到了输入框右边的 百度一下 按钮,并通过 click 方法点击该按钮。
2.2 元素定位及操作
元素定位与操作时自动化测试的核心,测试用例本质上就是通过操作元素来实现一些列测试流程。
2.2.1 Selenium 元素定位及元素操作(包含 Web 表单)
- Selenium WebDriver API 支持使用以下八种定位器来定位元素
- WebDriver API 中提供了
find_element
和find_elements
方法来查找一个或者多个元素,该方法有两个参数,第一个参数就是查找的方式也就是我们前面提到的定位器,通过By
来指定,第二个参数就是指定的属性所对应的属性值。
元素定位
1、ID 定位
属性查找方式?
操作代码:
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
input_ele = driver.find_element(By.ID, '__BVID__21')
input_ele.send_keys('Selenium')
2、NAME 定位
操作代码:
<html>
<body>
<label>用户名</label>
<input type="text" name="username">
<label>密码</label>
<input type="password" name="password">
<br>
<button name="submit">登录</button>
</body>
</html>
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
path = 'file://<login.html文件的绝对路径>'
driver.get(path)
username = driver.find_element(By.NAME, 'username')
password = driver.find_element(By.NAME, 'password')
button = driver.find_element(By.NAME, 'submit')
username.send_keys('lanqiao')
3、LINK_TEXT 和 PARTIAL_LINK_TEXT 定位
LINK_TEXT 和 PARTIAL_LINK_TEXT 常用于定位 a 标签元素。
操作代码:
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
discuss = driver.find_element(By.LINK_TEXT, '讨论区')
discuss.click()
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
discuss = driver.find_element(By.LINK_TEXT, '讨论区')
discuss.click()
4、CLASS_NAME 定位
属性查找方式?
操作代码:
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
login = driver.find_element(By.CLASS_NAME, 'signin')
login.click()
5、TAG_NAME 定位(元素标签名)
操作代码:
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
input = driver.find_element(By.TAG_NAME, 'input')
input.send_keys('Selenium')
6、XPATH 定位
打开 Chrome 浏览器的检查功能,在元素界面选中一个元素后点击鼠标右键,在弹出框中选择复制->复制完整 Xpath(绝对路径),就可以获取元素的完整 Xpath 表达。复制Xpath(相对路径)。
操作代码:
绝对路径
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
input = driver.find_element(By.XPATH, '/html/body/div/div/div/div[1]/div[1]/div[1]/form/input')
input.send_keys('XPATH')
相对路径
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
input = driver.find_element(By.XPATH, '//*[@id="__BVID__20"]')
input.send_keys('XPATH')
元素索引
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
input = driver.find_element(By.XPATH, '//input[1]')
input.send_keys('Xpath index')
元素属性
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
button = driver.find_element(By.XPATH, '//div[@class="sign-btn signup"]')
button.click()
7、CSS Selector
操作代码:
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
input_ele = driver.find_element(By.CSS_SELECTOR, '#__BVID__20')
input_ele.send_keys('css')
8、定位 iFrame 表单
操作代码:
<html>
<body>
<div id="modal">
<iframe id="buttonframe" name="myframe" src="https://www.lanqiao.cn/">
<button>Click here</button>
</iframe>
</div>
</body>
</html>
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select
import time
driver = webdriver.Chrome()
driver.get('file://<iframe.html文件的绝对路径>')
# 定位 iframe
iframe = driver.find_element(By.CSS_SELECTOR, "#modal > iframe")
# driver 切换到 定位到的 iframe 中
driver.switch_to.frame(iframe)
time.sleep(3)
# 再定位 iframe 中的按钮
driver.find_element(By.TAG_NAME, 'button').click()
time.sleep(3)
driver.quit()
元素操作
- click(): 适用于任何元素,对其进行点击操作
- send_keys(): 适用于文本区域或者可编辑的元素,可以输入指定内容
- clear(): 适用于文本区域或者可编辑的元素,可以清空文本内容
- submit(): 适用于 Form 表单元素,用于提交数据,Selenium 4 中不再推荐使用此方法,而是推荐直接点检表单的提交按钮
- select: 选择单选或者多选框中的元素
1、click()
操作代码:
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
# 定位 讨论区 ,点击
discuss_link_ele = driver.find_element(By.PARTIAL_LINK_TEXT, '讨论')
discuss_link_ele.click()
time.sleep(3)
# 定位 登录按钮
login_button_ele = driver.find_element(By.CLASS_NAME, 'signin')
login_button_ele.click()
2、send_keys() 和 clear()
操作代码:
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
# 定位 搜索输入框
search_ele = driver.find_element(By.TAG_NAME, 'input')
search_ele.send_keys('Selenium')
time.sleep(3)
# 清除输入的文本内容
search_ele.clear()
time.sleep(2)
driver.quit()
3、screenshot() 方法 与 save_screenshot()
WebDriver 还提供了截图相关的 API,分别是应用于元素截图的 screenshot() 方法,该方法可以截取指定元素的图片,由 WebElement 对象所调用,而 save_screenshot() 方法则可以截屏整个浏览器,有 WebDriver 对象调用。
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
# 定位 搜索输入框
search_ele = driver.find_element(By.TAG_NAME, 'input')
search_ele.send_keys('Selenium')
time.sleep(3)
# 截取 search 输入框的图片
search_ele.screenshot('./search_ele.png')
# 截取当前页面
driver.save_screenshot('./lanqiao_index.png')
time.sleep(3)
driver.quit()
4、Select
<html>
<body>
<div>
<select name="selectomatic">
<option selected="selected" id="non_multi_option" value="one">One</option>
<option value="two">Two</option>
<option value="four">Four</option>
<option value="still learning how to count, apparently">Still learning how to count, apparently</option>
</select>
</div>
<br>
<div>
<select name="multi" id="multi" multiple="multiple">
<option selected="selected" value="eggs">Eggs</option>
<option value="ham">Ham</option>
<option selected="selected" value="sausages">Sausages</option>
<option value="onion gravy">Onion gravy</option>
</select>
</div>
</body>
</html>
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select
import time
driver = webdriver.Chrome()
driver.get('file://<select.html 文件绝对路径>')
select_element = driver.find_element(By.NAME, 'selectomatic')
select_obj = Select(select_element)
# 获取所有的选项元素,列表形式
options_ele_list = select_obj.options
# 输出第一个元素的文本值
print(select_obj.options[0].text) # 输出 One
# 获取已被选择的元素
selected_options = select_obj.all_selected_options
print(selected_options[0].text) # 输出 One
# 选择元素
select_obj.select_by_index(2)
time.sleep(3)
select_obj.select_by_value('two')
time.sleep(3)
select_obj.select_by_visible_text('Four')
time.sleep(3)
driver.quit()
2.2.2 Selenium 的等待
显示等待
隐式等待
强制等待
from selenium import webdriver
from selenium.webdriver.common.by import By
# 导入time模块,实现强制等待
import time
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
# 进入首页后,强制线程休眠 3s,待页面元素全部加载完毕后再进行定位操作
time.sleep(3)
search_ele = driver.find_element(By.TAG_NAME, 'input')
search_ele.send_keys('Selenium')
# 再次休眠 3s
time.sleep(3)
driver.quit()
2.2.3 Selenium 浏览器操作
浏览器窗口的操作
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
# 访问 百度首页
driver.get('https://www.baidu.com/')
# 打印出默认窗口大小
print(driver.get_window_size())
# 设置浏览器窗口大小
driver.set_window_size(500, 800)
# 接着访问 蓝桥首页
driver.get('https://www.lanqiao.cn/')
# 后退到baidu首页
driver.back()
# 前进到蓝桥首页
driver.forward()
# 刷新页面
driver.refresh()
# 获取窗口位置
print(driver.get_window_position())
# 获取页面的 Title 属性
print(driver.title)
# 获取页面的 HTML 源代码
driver.page_source
切换浏览器窗口句柄
下述代码,通过 WebDriver 的 window_handles 属性来获取所有的句柄,返回一个包含句柄的列表,通过列表的索引操作来获取最新打开的句柄(最新打开的一个 Tab 页),并且通过 switch_to 方法切换到最新的 Tab 页,从输出的页面标题信息可以确定,driver 从搜索结果页面切换到了蓝桥云课官网页面,如果不使用 switch_to 方法切换句柄的话,driver 还停留在搜索结果列表页面。
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
driver = webdriver.Chrome()
# 访问 百度首页
driver.get('https://www.baidu.com/')
driver.find_element(By.ID, 'kw').send_keys('蓝桥云课')
driver.find_element(By.ID, 'su').click()
time.sleep(3)
driver.find_element(By.PARTIAL_LINK_TEXT, '连接高校和企业 - ').click()
time.sleep(3)
# 输出当前 handle 信息,输出页面标题
print(driver.title)
# 获取所有句柄
handles = driver.window_handles
lanqiao_handle = handles[-1]
# 切换 handle
driver.switch_to.window(lanqiao_handle)
print(driver.title)
2.2.4 Selenium 与 JS 执行
使用 JS 代码操作页面元素
from selenium import webdriver
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
searchInputJS = 'document.getElementById("__BVID__20").value="Selenium"'
driver.execute_script(searchInputJS)
上述代码中通过 JS 中的 getElementById 方法定位到了蓝桥云课首页的输入框,并通过 value 属性,定义了输入框中的内容为 "Selenium"
使用 JS 控制滚动条
WebDriver API 是无法操作页面右侧的滚动条的,这个时候可以已使用执行 JS 脚本的方式来控制页面的滚动条。
from selenium import webdriver
driver = webdriver.Chrome()
driver.get('https://www.lanqiao.cn/')
#通过 JS 中的 scrollTo 函数来控制滚动条滚动至页面的最下方法
scrollToJS = 'window.scrollTo(100, document.body.scrollHeight);'
driver.execute_script(scrollToJS)
下述代码通过指定 JS 的方式将指定的蓝桥云课首页底部的职场提升路径显示到当前屏幕上,scrollIntoView 方法有元素调用,scrollIntoView(true) 表示将元素滚动到屏幕中间,scrollIntoView(false) 表示将元素滚动到屏幕底部。
from selenium import webdriver
driver = webdriver.Chrome()
driver.maximize_window()
driver.get('https://www.lanqiao.cn/')
scrollIntoView = 'document.getElementsByClassName("path-item")[1].scrollIntoView(false);'
driver.execute_script(scrollIntoView)
2.3 Selenium 自动化测试实战
2.3.1 Selenium 自动化测试实战
from selenium import webdriver
from selenium.webdriver.common.by import By
# 打开首页
driver = webdriver.Chrome()
# 设置隐式等待
driver.implicitly_wait(10)
driver.get('https://www.lanqiao.cn/')
# 跳转到登录页面
driver.find_element(By.CLASS_NAME, 'signin').click()
# 点击手机登录,进入到密码登录页面
driver.find_element(By.XPATH, '//div[3]/span').click()
# 输入用户名密码,点击登录
inputs = driver.find_elements(By.TAG_NAME, 'input')
# 用户名
inputs[0].send_keys('18268046852')
# 密码
inputs[1].send_keys('1qaz@WSX')
# 登录
driver.find_element(By.CLASS_NAME, 'ant-btn-primary').click()
# 关闭弹窗
driver.find_element(By.CLASS_NAME, 'btn-close').click()
# 用例1:查看提示消息
driver.find_element(By.CLASS_NAME, 'messages-wrap').click()
# 用例2:继续学习
driver.get('https://www.lanqiao.cn/')
driver.find_element(By.CSS_SELECTOR, 'div[class="user-box"]').click()
print(driver.title)
# 切换句柄
handles = driver.window_handles
driver.switch_to.window(handles[-1])
print(driver.title)
# 获取继续实验按钮
buttons = driver.find_elements(By.LINK_TEXT, '继续实验')
print(len(buttons))
buttons[0].click()
2.3.2 使用 Unittest 组织测试用例
Python 单元测试框架 unittest
unittest 测试框架是标准 Python 语言中的一个模块,是 Python 自带的单元测试框架,主要用于 Python 程序的自动化测试。unittest 支持自动化测试以及 setUp 和 tearDown 模块,可以集合所有的测试用例并且将结果独立的展示在报告中。
-
测试脚手架 test fixture test fixture 表示为了开展一项或多项测试所需要进行的准备工作,以及所有相关的清理操作。举个例子,这可能包含创建临时或代理的数据库、目录,再或者启动一个服务器进程。
-
测试用例 test case 一个测试用例是一个独立的测试单元。它检查输入特定的数据时的响应。 unittest 提供一个基类: TestCase ,用于新建测试用例。
-
测试套件 test suite test suite 是一系列的测试用例,或测试套件,或两者皆有。它用于归档需要一起执行的测试。
-
测试运行器 test runner test runner 是一个用于执行和输出测试结果的组件。这个运行器可能使用图形接口、文本接口,或返回一个特定的值表示运行测试的结果
1、测试用例 Test Case
2、测试用例的执行顺序
3、跳过某个测试用例
4、断言 Assert
执行测试用例过程中,我们需要通过断言来判断测试用例是否执行成功,通过测试得到的实际结果与测试用例的期望结果进行比较判断是否执行成功,unittest 框架的 TestCase 类提供的用于断言的方法如下:
方法 | 检查 |
---|---|
assertEqual(a, b) | a == b |
assertNotEqual(a, b) | a != b |
assertTrue(x) | bool(x) is True |
assertFalse(x) | bool(x) is False |
assertIs(a, b) | a is b |
assertIsNot(a, b) | a is not b |
assertIsNone(x) | x is None |
assertIsNotNone(x) | x is Not None |
assertIn(a, b) | a in b |
assertNotIn(a, b) | a not in b |
assertIsInstance(a, b) | isinstance(a, b) |
assertNotIsInstance(a, b) | not isinstance(a, b) |
使用 unittest 组织测试用例
下述代码中我们分别创建了两个类,一个是 TestLogin 用户测试登录功能,另一个是 TestCheckSysNews 用于测试查看系统消息功能,在使用 unittest 进行自动化时建议一个类对应一个功能,一个功能下有多条测试用例,多条测试用例可以分别使用不同的方法来表示,当然这些方法都要使用 test_ 开头来表示。
from selenium import webdriver
from selenium.webdriver.common.by import By
import unittest
class TestLogin(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Chrome()
self.base_url = 'https://www.lanqiao.cn/'
def tearDown(self):
self.driver.quit
def test_login(self):
self.driver.implicitly_wait(10)
# 登录
# 跳转到登录页面
self.driver.get(self.base_url)
self.driver.find_element(By.CLASS_NAME, 'signin').click()
# 点击手机登录,进入到密码登录页面
self.driver.find_element(By.XPATH, '//div[3]/span').click()
# 输入用户名密码,点击登录
inputs = self.driver.find_elements(By.TAG_NAME, 'input')
# 用户名
inputs[0].send_keys('18268046852')
# 密码
inputs[1].send_keys('xxxx')
# 登录
self.driver.find_element(By.CLASS_NAME, 'ant-btn-primary').click()
# 关闭弹窗
self.driver.find_element(By.CLASS_NAME, 'btn-close').click()
username_ele = self.driver.find_element(By.CSS_SELECTOR, 'div.user > div > div > span.name')
# 断言用户名
self.assertEqual(username_ele.text, 'Riemann')
class TestCheckSysNews(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Chrome()
self.base_url = 'https://www.lanqiao.cn/'
# 登录
# 跳转到登录页面
self.driver.implicitly_wait(10)
self.driver.get(self.base_url)
self.driver.find_element(By.CLASS_NAME, 'signin').click()
# 点击手机登录,进入到密码登录页面
self.driver.find_element(By.XPATH, '//div[3]/span').click()
# 输入用户名密码,点击登录
inputs = self.driver.find_elements(By.TAG_NAME, 'input')
# 用户名
inputs[0].send_keys('18268046852')
# 密码
inputs[1].send_keys('xxx')
# 登录
self.driver.find_element(By.CLASS_NAME, 'ant-btn-primary').click()
# 关闭弹窗
self.driver.find_element(By.CLASS_NAME, 'btn-close').click()
def tearDown(self):
self.driver.quit()
def test_check_sys_news(self):
self.driver.implicitly_wait(10)
self.driver.find_element(By.CLASS_NAME, 'messages-wrap').click()
self.assertEqual(self.driver.title, '系统通知 - 蓝桥云课')
if __name__ == '__main__':
suit = unittest.TestSuite()
suit.addTest(TestLogin('test_login'))
suit.addTest(TestCheckSysNews('test_check_sys_news'))
runner = unittest.TextTestRunner()
runner.run(suit)
2.4 框架设计
2.4.1 PO模式介绍
PO 即 Page Object 页面对象,PO 模式实现了页面元素定位和测试用例的分层,大大降低了代码的耦合,提高了代码的可维护性,通过 BasePage 来封装一些基本的操作方法,并且所有的 Page 对象都继承 BasePage,这样分层的好处是即使 UI 层面有变化,我们只需要修改 Page 对象即可,无需修改测试用例。
创建一个文件夹 pages,用来存放所有的 Page 对象,在 pages 包中创建 __init__.py 文件,该文件中不需要添加任何内容,通过 __init__.py 的添加将 pages 目录变成了一个 python 的包。接着在 pages 包中创建 login_page.py 文件,并在该文件中创建 LoginPage 来表示登录页面的 Page 对象,具体代码如下:
# Lanqiao Login Page
from selenium.webdriver.common.by import By
class LoginPage:
def __init__(self, driver):
self.driver = driver
def input_username(self, username):
self.driver.find_elements(By.TAG_NAME, 'input')[0].send_keys(username)
def input_password(self, password):
self.driver.find_elements(By.TAG_NAME, 'input')[1].send_keys(password)
def click_login(self):
self.driver.find_element(By.CLASS_NAME, 'ant-btn-primary').click()
下述代码中,首先导入了 pages 包中的 login_page.py 模块中的 LoginPage 类,在具体的测试用例中,通过传入 driver 来实例化 page 对象,通过调用 page 对象的方法对元素进行操作,从而避免了在测试用例中硬编码元素定位相关信息,这样即使 UI 发生了变化,我们只需要对 Page 对象进行更改即可,而无需修改测试用例。
from pages.login_page import LoginPage
from selenium import webdriver
import unittest
import time
from HTMLTestRunner import HTMLTestRunner
class TestLogin(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Chrome()
self.base_url = 'https://passport.lanqiao.cn/login'
def tearDown(self):
time.sleep(3)
self.driver.quit()
def test_loing_case1(self):
self.driver.implicitly_wait(10)
self.driver.get(self.base_url)
login_page = LoginPage(self.driver)
login_page.input_username('18268046852')
login_page.input_password('1qaz#EDC')
login_page.click_login()
time.sleep(2)
title = self.driver.title
self.assertEqual(title, '用户信息')
def test_loing_case2(self):
self.driver.implicitly_wait(10)
self.driver.get(self.base_url)
login_page = LoginPage(self.driver)
login_page.input_username('18268046852')
login_page.input_password('1qaz@WSX')
login_page.click_login()
time.sleep(2)
title = self.driver.title
self.assertEqual(title, '用户登录')
if __name__ == '__main__':
# 创建测试套件
suit = unittest.TestSuite()
suit.addTest(TestLogin('test_loing_case1'))
suit.addTest(TestLogin('test_loing_case2'))
runner = unittest.TextTestRunner()
runner.run(suit)
2.4.2 BasePage 实现及封装
2.4.3 自动截图实现
2.4.4 参数化以及数据驱动实现
2.4.5 HTML 测试报告集成
第三阶段:单元测试
3.1 白盒测试(逻辑覆盖法)
3.1.1 单元测试
单元测试是对软件的基本组成单元进行的测试,如函数、类或类的方法。单元测试的依据是模块的详细设计文档,主要关注一个单元是否正确实现了规定的功能、代码逻辑是否正确、输入输出是否正确、代码是否符合相关的标准规范等。
3.1.2 语句覆盖法
语句覆盖法是指设计适当数量的测试用例,使被测程序中的每条语句至少被执行一次。
3.1.3 分支(判定)覆盖法
分支(判定)覆盖法是指设计适当数量的测试用例,运行被测程序,使得程序中每个判定语句的真、假分支至少被执行一次。流程图中一个菱形框就是一个判定语句,分支(判定)覆盖就是要设置一些测试用例使判定语句为 Y 和 N 的分支都至少被执行一次。
实验:
public static int test(int a,int b,int c) {
1 int result = 0;
2 if(a == 0 or b > 2) {
3 result = b - a;
}
4 if(a > 0 and c > 0 ) {
5 result = c * a;
}
6 return result;
}
第一步:画出流程图
第二步:分析流程图,编写测试用例
根据分支(判定)覆盖的定义,我们需要设计一些测试用例,使程序中的每个判定条件至少被执行一次,即上图中的判断语句 ② 和 ③ 的真、假分支都需要至少被执行一次。因此,我们可以设计如下表中的两个测试用例覆盖所有的真、假分支。
3.1.4 条件覆盖
条件覆盖法是指设计适当数量的测试用例,运行被测程序,使得程序中每个判断语句中条件的真、假分支至少被执行一次。
3.1.5 分支-条件覆盖
分支-条件覆盖,也叫判定-条件覆盖,是指运行代码进行测试时,程序中所有判断语句中的条件取值为真、取值为假的情况和整个判断语句取真分支、假分支的情况都被覆盖到(即,至少被执行过一次)。
3.1.6 条件组合覆盖
条件组合覆盖又称为多条件覆盖,是指设计足够数据的测试用例,使每个判定语句中的所有判定条件的各种可能的组合都至少被执行一次。
3.1.7 路径覆盖法
路径覆盖法是指设计一定数量的测试用例运行被测程序,使程序中的所有路径都至少被执行一次。
3.1.8 基本路径覆盖法
基本路径覆盖法是在程序控制流图的基础上,通过分析控制结构的圈复杂度,导出基本可执行的路径集合设计测试用例,运行被测程序,使程序的基本路径都得到覆盖。
实验:
public static int test(int a,int b,int c) {
1 int result = 0;
2 if(a == 0 or b > 2) {
3 result = b - a;
}
4 if(a > 0 and c > 0 ) {
5 result = c * a;
}
6 return result;
}
第一步:画流程图
第二步:在控制流图中如果含有复合条件,需要改为单条件嵌套的形式
第三步:根据流程图画出控制流图。
第四步:计算圈复杂度。
法一:V(G) = P + 1,其中 P 代表控制流图中的判定节点数。从下图可以看出,程序的控制流图中共有 4 个判定节点,所以,圈复杂度 V(G) = 4 + 1 = 5 。
法二:V(G) = A + 1,其中 A 代表控制流图中的封闭区域数量。从下图可以看出,程序的控制流图中共有 4 个封闭区域,所以,圈复杂度 V(G) = 4 + 1 = 5 。
法三:V(G) = e - n + 2,其中 e 代表控制流图中的边的数量,即控制流中的箭头数量;n 代表控制流图的节点数量,即控制流图中的圆圈数量。从下图中可以看出,程序的控制流图中有 11 条边(11个箭头),8个节点(8个圆圈),所以,圈复杂度 V(G) = 11 - 8 + 2 = 5 。
第五步:确定基本路径的集合。
基本路径又称为独立路径,是指至少包含一条其他独立路径中未包含的路径。例如,在上图中,路径 1 - 2 - 3 - 5 - 8 是一条基本路径,1 - 2 - 4 - 3 - 5 - 8 则可以看成了另外一条基本路径,因为这条路径中经过 4 节点的路径在第一条基本路径中未包含。
圈复杂度是指程序中的独立路径数量,是确保程序中每个可执行语句至少执行一次需要的测试用例数量的最小值
第六步:根据基本路径编写测试用例。
3.2 白盒测试(循环语句覆盖法)
3.2.1 简单循环
简单循环是最简单的循环,即只有一个循环且没有嵌套,例如,一个 while 循环、一个do-while 循环、一个 for 循环。下图是两种简单循环的示意图:
实验:求任意一个10以内整数的阶乘
public static int getFactorial(Integer num) {
int result = 0;
if (num >= 1 && num <= 10){
result = 1;
int i = 1;
while (i < num){
result = result * i;
i++;
}
System.out.println(num + "的阶乘为:" + result);
}
else{
System.out.println("请输入1~10的整数!");
}
return result;
}
第一步:画出流程图:
第 2 步:设计测试用例:
简单循环的测试用例需要考虑下列几种情况(设最大循环次数为 n ):
(1)循环 0 次:测试跳过整个循环的场景;
(2)循环 1 次:目的是检查循环的初始值是否正确;
(3)循环 2 次:目的是检查多次循环是否正确;
(4)循环 m 次(其中 2 < m < n - 1):目的是检查多次循环是否正确,这里我们也可以用等价类的思想来理解,即:可以把大于 2 次、小于 n - 1 次看成是一个等价类,m 可以是这个范围中的任意一个值,根据等价类的思想,如果这个范围中的任意一个值是不会发现程序的问题,那么,我们可以认为这个等价类中所有的值都不会发现程序的问题;
(5)循环 n - 1 次:目的是检查边界值是否正确;
(6)循环 n 次:目的是检查边界值是否正确;
(7)循环 n + 1 次:目的是检查边界值是否正确。这里读者可能会有疑问,一个循环的最大循环次数是 n ,我们要怎么让它循环 n + 1 次呢?这不是一个伪命题吗?通过对边界值方法的理解,我们可以知道,等于、大于、小于边界值的地方是最容易出现 bug 的,如,“差 1 错”,即不正确的多循环或者少循环了一次。在循环结构的测试中设计循环 n + 1次的测试用例,就是为了检查代码是否会出现多循环一次的错误。在实际的测试过程中,我们可以通过分析代码结构决定是否能设计出循环 n + 1次的测试用例。
3.2.2 嵌套循环
嵌套循环是指一个循环语句的循环体内含有其他的循环语句的语法结构,while、for 等循环语句都可以进行嵌套。最常见的嵌套循环是 for 循环中嵌套 for 循环。嵌套循环执行时,外层循环每执行一次内层循环会执行多次,循环的总次数等于外层循环次数与内层循环次数的积。下面是一个嵌套循环的示意图:
实验:冒泡排序
//冒泡排序
public static int[] bubble_sort(int[] numbers){
for (int i = 0; i < numbers.length - 1;i++ ){
boolean flag = false;
for (int j = 0;j < numbers.length - 1 - i;j++){
if (numbers[j] > numbers[j+1]){
int temp = 0;
temp = numbers[j];
numbers[j] = numbers[j+1];
numbers[j+1] = temp;
flag = true;
}
}
if (flag == false){
break;
}
}
return numbers;
}
第一步:画出流程图
3.2.3 串接循环
3.3 Unittest 介绍及环境搭建
3.3.1 Python 单元测试框架介绍
单元测试是对软件的基本组成单元(函数、类、方法等)进行的测试。单元测试框架可以帮助我们组织、执行测试用例,并提供断言等功能帮助我们实现单元测试的自动化。
unittest
支持测试自动化、配置共享、将测试用例聚合到测试集中并将测试结果独立展示。
pytest
pytest 官方网站地址为:pytest: helps you write better programs — pytest documentation 。
nose
nose 官方参考文档地址为: Note to Users — nose 1.3.7 documentation 。
doctest
doctest 官方文档,地址为:doctest — Test interactive Python examples — Python 3.12.2 documentation 。
3.3.2 unittest 介绍
unittest 与其他语言中的主流单元测试框架(如 Junit)有着相似的风格,支持测试自动化、配置共享(例如,使用 setUp 实现测试用例前的初始化工作、使用 tearDown 实现测试结束后的清理回收工作等)、将测试用例聚合到测试集中并将测试结果独立展示。除了进行 Python 代码单元测试,unittest 测试框架也可以与其他 UI 、接口测试等工具相结合,完成自动化测试用例的组织、执行和测试结果的生成。
1、unittest 测试框架中包含四个重要的概念
- TestCase(测试用例)
TestCase 是指独立的测试单元,它可以针对特定的输入数据检查程序的响应及输出结果是否符合预期,也可以理解成一个 TestCase 的实例就是一个测试用例。 unittest 提供了一个基类 TestCase ,用于新建测试用例。如果我们要使用 unittest 新建 TestCase ,可以通过继承 TestCase
基类来实现。如果一个测试类继承了 unittest.TestCase 类,它就是一个测试用例,例如,下列代码是一个用来测试 add() 方法的 TestCase :
class TestCalculator(unittest.TestCase):
def test_add(self):
result = add(3, 5)
self.assertEqual(result, 8)
需要注意的是:如果一个测试类继承了 unittest.TestCase 类,而这个测试类中有多个以 test
开头的测试方法,那么,执行测试用例加载的时候会为每一个测试方法生成一个 TestCase 实例。
- TestFixture(测试固件,也称为测试脚手架)
TestFixture 是指一个或多个测试用例执行前的准备工作及执行后的清理工作,相当于测试用例的前置条件和后置条件。例如,执行用例前创建数据库连接、执行后进行垃圾回收等。
- TestSuite(测试套件)
TestSuite 是指一系列测试用例 / 测试套件的集合。它的作用是批量执行测试用例。
- TestRunner(测试运行器)
TestRunner 是一个用于执行测试用例和输出测试结果的组件。
2、unittest 的工作原理
- 运行测试时,
TestLoader
将TestCase
加载到TestSuite
中; - 加载好以后,由
TextTestRunner
运行TestSuite
,并将运行的结果保存在TextTestResult
中。
具体如下图如示:
3.3.3 为什么要使用 unittest 单元测试框架
- 可以高效地管理及执行测试用例
- 提供了丰富的断言方法
- 提供了丰富的日志信息
3.3.4 unittest 语法规则
- 类、方法等的命名不使用 unittest 关键字
- 测试类都需要继承 unittest.TestCase 类
- 测试类中的测试方法名称一般使用 test 开头
3.4 Unittest 四大要素
3.4.1 TestCase
TestCase 是 unittest 单元测试框架中最重要的概念之一,使用 TestCase 类可以快速创建测试用例,帮助单元测试人员提高测试工作效率。TestCase 是 unittest 中最小的可测试单元,它可以针对特定的输入数据检查程序的响应及输出结果是否符合预期。一个 TestCase 实例可以理解成就是一个测试用例。unittest 测试框架中提供了一个基类 TestCase ,用于创建测试用例。
unittest 中编写测试用例的主要步骤为:
- 导入 unittest 模块;
- 创建继承 unittest.TestCase 基类的测试类;
- 在测试类中分别为每个测试用例编写方法名以 test 开头的测试方法。
解读一个测试用例代码:
# 导入unittest模块
import unittest
# 手动添加被测试模块的依赖路径
import sys
target_path="./"
sys.path.append(target_path)
# 导入被测试模块
from src.CalculateScore import *
# 测试类
class scoresTest(unittest.TestCase):
# 测试用例scores01
def test_scores01(self):
self.assertTrue(self, CalculateScore.scores(95) == 'A')
# 测试用例scores02
def test_scores02(self):
self.assertTrue(self, CalculateScore.scores(80) == "B")
# 测试用例scores03
def test_scores03(self):
self.assertTrue(self, CalculateScore.scores(79) == "C")
# 测试用例scores04
def test_scores04(self):
self.assertTrue(self, CalculateScore.scores(58) == "D")
1)使用 unittest 编写测试用例前需先导入 unittest 模块:
import unittest
2)导入被测试模块( src 目录下的 CalculateScore.py ):
from src.CalculateScore import *
3)直接使用上述代码导入被测试模块 src.CalculateScore ,运行测试时会报没有找到 src 模块的错误,所以,需要在导入被测试模块( src 目录下的 CalculateScore.py )之前手动添加被测试模块的依赖路径,即在导入被测试模块的代码前增加下列代码。这里直接使用 " ./ " 通过相对路径来导入 firstDemo
import sys
target_path="./"
sys.path.append(target_path)
4)创建继承 unittest.TestCase 基类的测试类,格式为:class 测试类名(unittest.TestCase):
。如果一个测试类继承了 unittest.TestCase 类,它就是一个测试用例。需要注意的是:如果一个测试类继承了 unittest.TestCase 类,而这个测试类中有多个以 test.
开头的测试方法,那么,执行测试用例加载的时候会为每一个测试方法生成一个 TestCase 实例。
5)在测试类中编写测试方法,将被测试类 CalculateScore 中 scores 方法的每一个测试用例转化为测试方法。测试方法一般以 test_ 开头,除了 self 以外不包含额外的参数。测试方法中主要完成以下工作:
-
使用测试用例的输入数据作为入参调用被测试方法;
-
比较程序的实际返回结果与测试用例的预期结果是否一致,如果一致则测试执行通过,否则测试执行失败。在本例中,我们使用了 assertTrue() 断言方法来判断实际输出与预期结果是否一致,例如,下面是第一个测试用例中的断言语句。unittest 中还有很多断言方法可以帮助我们判断测试结果,这些断言方法将在本章后面的章节中详细介绍.
self.assertTrue(self, CalculateScore.scores(95) == 'A')
3.4.2 TestFixtures
TestFixture(测试固件),也称为测试脚手架,主要用于测试用例执行前的准备工作及执行后的清理工作,相当于测试用例的前置条件和后置条件。
方法级别:setUp() 与 tearDown()
setUp() 与 tearDown() 方法编写在测试类中。
setUp() 方法会在该类中的每一个测试方法执行前各执行一次,一般用于初始化测试用例数据。
tearDown() 方法会在该测试类中的每一个测试方法执行后各执行一次,一般用来处理测试用例执行完成之后的清理工作。
类级别:setUpClass() 与 tearDownClass()
setUpClass() 与 tearDownClass() 方法也是编写在测试类中。
setUpClass() 方法会在该测试类中的所有测试方法执行前执行一次,一般可用于初始化一些共享的配置方法(例如,连接到数据库)。
tearDownClass() 方法会在该测试类中的所有测试方法执行后执行一次,执行完测试类中的所有测试方法之后执行某些操作(例如,断开数据库连接)。
模块级别:setUpModule() 与 tearDownModule()
setUpModule() 与 tearDownModule() 方法编写在测试类外面。setUpModule() 方法会在整个模块中的所有测试类执行前执行一次,tearDownModule() 方法会在整个模块中的所有测试类执行后执行一次。
3.4.3 TestSuite
- 使用 addTest() 方法添加单条测试用例至测试套件中
- 使用 addTests() 方法添加多条测试用例至测试套件中
- 使用 TestLoader 类添加测试用例至测试套件中
- 使用 TextTestRunner 中的 run() 方法执行测试套件
我们可以使用其中的 loadTestsFromTestCase() (类装载器)、loadTestsFromModule() (模块装载器)及 loadTestsFromName() (名称装载器)等方法来添加测试用例至测试套件中。
loadTestsFromTestCase() 方法可以根据指定的测试类名获取该类中以 test 开头的所有测试方法,并返回这些测试方法的集合。
loadTestsFromModule() 方法返回 unittestDemo.py 中所有以 test 开头的测试方法集合
3.4.4 TestRunner
TestRunner 也是 unittest 单元测试框架中的一个重要组件,它主要用于执行测试用例和输出测试结果。
执行测试用例
# 实例化 TextTestRunner 类
runner = unittest.TextTestRunner()
# 运行测试套件
runner.run(测试套件名)
输出测试结果
3.5 Unittest 断言
在单元测试过程中可以通过断言来判断测试用例的执行结果。
1、assertEqual与assertNotEqual
用于测试两个值是否相等或不相等。
2、assertTrue与assertFalse
用于检查一个条件是否为真或为假。
3、assertIs 与 assertIsNot
用于检查两个对象是否引用了同一个对象。
4、assertIsNone 与 assertIsNotNone
用于检查表达式的结果是否为 “None” 。
5、assertIn 与 assertNotIn
用于检查两个参数是否存在包含关系。
6、assertIsInstance 与 assertNotIsInstance
用于检查对象是否为指定类的实例。
举例:
import unittest
# 测试类1
class Class1(object):
@classmethod
def test1(cls, a, b):
return a + b
# 测试类2
class Class2(object):
@classmethod
def test2(cls, a, b):
return a - b
class assertionDemo(unittest.TestCase):
# 实例化测试类1
class1 = Class1()
def test_01(self):
# 断言实例对象 class1 是否为类 class1 的实例
self.assertIsInstance(self.class1, Class1)
#测试方法 test_01() 中使用的是 assertIsInstance() 方法断言对象 class1 是否为类 Class1 的实例,测试执行结果为通过;
def test_02(self):
# 断言实例对象 class1 是否为类 class2 的实例
self.assertIsInstance(self.class1, Class2)
#测试方法 test_02() 使用的是 assertIsInstance() 方法断言对象 class1 是否为类 Class2 的实例,因为 class1 是类 Class1 的实例,所以执行结果为失败;
def test_03(self):
# 断言实例对象 class1 是否不为类 class1 的实例
self.assertNotIsInstance(self.class1, Class1)
def test_04(self):
# 断言实例对象 class1 是否不为类 class2 的实例
self.assertNotIsInstance(self.class1, Class2)
if __name__ == '__main__':
unittest.main()
3.6 Unittest 测试用例执行
3.6.1 自定义测试用例执行顺序
在 unittest 中,测试用例默认是按照测试模块 / 测试类 / 测试方法名称的 ASCII 码升序执行的,可以使用 TestSuite 类中的 addTest() 方法按顺序将测试用例添加至测试套件,使测试用例按照我们期望的顺序来执行。
3.6.2 在 VS Code 中配置 unittest
3.6.3 异常测试
unittest 测试框架提供了断言异常的方法 assertRaises 和 assertRaisesRegex ,可以帮助我们完成异常测试。assertRaises 断言可以帮助我们验证程序抛出的异常类型是否与预期的一致,assertRaisesRegex 断言则可以验证异常类型及异常信息是否符合我们的预期。
使用 assertRaises 断言进行异常测试
import unittest
# 被测试代码
def division(a, b):
return a / b
# 测试类
class divisionTest(unittest.TestCase):
# 测试方法
def test_testcase01(self):
# 使用assertRaises断言验证除数为0时程序抛出的异常类型是否为ZeroDivisionError
self.assertRaises(ZeroDivisionError, division, 5, 0)
if __name__ == "__main__":
unittest.main()
assertRaises() 断言方法的语法为:assertRaises(exception, callable, *args, **kwds)
- exception :预期的异常类型;
- callable :被测试方法;
- args 、 kwds :被测试方法的参数。
使用 assertRaisesRegex 断言进行异常测试
import unittest
# 被测试代码
def division(a, b):
return a / b
# 测试类
class divisionTest(unittest.TestCase):
# 测试方法
def test_testcase02(self):
# 断言程序是否正确抛出异常(包括异常类型和异常信息)
self.assertRaisesRegex(ZeroDivisionError, "division by zero", division, 5, 0)
if __name__ == "__main__":
unittest.main()
assertRaisesRegex () 断言方法的语法为:assertRaisesRegex(exception, regexp, callable, *args, **kwds)
- exception :预期的异常类型;
- regexp :预期的匹配值,该匹配值可以是正则表达式对象或包含适合 re.search() 使用的正则表达式的 string 字符串;
- callable :被测试方法;
- args 、 kwds :被测试方法的参数。
3.6.4 忽略测试
从 Python 2.7 版本开始,unittest 增加了支持忽略测试的功能,通过使用这个功能,我们可以在测试执行过程中跳过或有条件地跳过某些测试类或测试方法。
- 无条件跳过测试
语法:@unittest.skip ( reason )
说明:该装饰器方法用于无条件地跳过某个测试类或测试方法。其中,参数 reason 是必须的,但它对测试运行没有影响,可以根据实际情况编写跳过原因,便于测试代码的维护。
import unittest
# 第一个测试类
class testSuiteTest1(unittest.TestCase):
def test_01(self):
print("这是第一个测试类的第 1 个测试用例!")
# 无条件跳过该测试方法
@unittest.skip("回归测试01")
def test_02(self):
print("这是第一个测试类的第 2 个测试用例!")
def test_03(self):
print("这是第一个测试类的第 3 个测试用例!")
# 无条件跳过该测试类
@unittest.skip("回归测试01")
# 第二个测试类
class testSuiteTest2(unittest.TestCase):
def test_01(self):
print("这是第二个测试类的第 1 个测试用例!")
def test_02(self):
print("这是第二个测试类的第 2 个测试用例!")
# 第三个测试类
class testSuiteTest3(unittest.TestCase):
# 无条件跳过该测试方法
@unittest.skip("回归测试01")
def test_01(self):
print("这是第三个测试类的第 1 个测试用例!")
def test_02(self):
print("这是第三个测试类的第 2 个测试用例!")
if __name__ == '__main__':
unittest.main()
- 有条件跳过测试
语法:@unittest.skipIf ( condition, reason )
说明:condition 为条件参数,如果 condition 为真则跳过测试。
import unittest
# 第一个测试类
class testSuiteTest(unittest.TestCase):
version = 1.0
def test_01(self):
print("这是第一个测试类的第 1 个测试用例!")
# 当 version == 2.0 时跳过该测试方法
@unittest.skipIf(version == 2.0, "用例不适用于2.0版本")
def test_02(self):
print("这是第一个测试类的第 2 个测试用例!")
# 当 version == 1.0 时跳过该测试方法
@unittest.skipIf(version == 1.0, "用例不适用于1.0版本")
def test_03(self):
print("这是第一个测试类的第 3 个测试用例!")
if __name__ == '__main__':
unittest.main()
语法:@unittest.skipUnless( condition, reason )
说明:除非 condition 为真,否则跳过测试。即 condition 为真时不跳过测试,为假时才跳过测试。
import unittest
# 第一个测试类
class testSuiteTest(unittest.TestCase):
version = 1.0
def test_01(self):
print("这是第一个测试类的第 1 个测试用例!")
# 除非 version == 2.0 ,否则跳过该测试方法
@unittest.skipUnless(version == 2.0, "用例只适用于2.0版本")
def test_02(self):
print("这是第一个测试类的第 2 个测试用例!")
# 除非 version == 1.0 ,否则跳过该测试方法
@unittest.skipUnless(version == 1.0, "用例只适用于1.0版本")
def test_03(self):
print("这是第一个测试类的第 3 个测试用例!")
if __name__ == '__main__':
unittest.main()
3.6.5 超时测试
unittest 中,可利用 timeout-decorator 模块中的超时装饰器来设置测试方法的超时时间,但是 timeout-decorator 模块中的 signal 包只支持 Linux 和 Mac 系统,Windows 平台下无法使用。而func_timeout 模块中的 @func_set_timeout() 超时装饰器方法则可以同时支持各种操作系统的超时设置。
import unittest
from func_timeout import func_set_timeout
# 被测试代码
def endlessLoop():
while (1 == 1):
continue
# 测试类
class TimeoutTest(unittest.TestCase):
# 测试方法
#在需要进行超时测试的方法上添加超时装饰器方法 @func_set_timeout() ,括号中的数字代表超时时间,单位为秒:
@func_set_timeout(3)
def test_endlessLoop(self):
print("超时测试开始……")
endlessLoop()
print("超时测试结束……")
if __name__ == '__main__':
unittest.main()
3.6.6 TestLoader 批量执行测试用例
在 unittest 测试框架中,除了使用测试套件的方式,TestLoader 类中的 discover() 方法也可以实现批量执行测试用例,当一次需要执行的测试用例非常多时,这种方式可以非常高效地完成测试用例的组织与执行。
import unittest
if __name__ == "__main__":
# 获取项目test目录下所有以test开头的测试模块中的测试用例
testcases = unittest.TestLoader().discover("test", pattern="test*.py", top_level_dir=None)
# 实例化TextTestRunner类
runner = unittest.TextTestRunner()
# 运行测试用例
runner.run(testcases)
代码解读:
1)使用 TestLoader().discover() 方法获取指定的测试用例。discover() 方法有三个参数:
- case_dir :待执行测试用例的目录。本例中测试用例放在了项目的 test 文件夹下,所以这里第一个参数写的是 "test" ;
- pattern :匹配测试用例文件的规则。pattern="test*.py" 表示匹配指定目录下文件名称以 test 开头的 .py 文件;
- top_level_dir :顶层目录名称,一般默认为 None。
testcases = unittest.TestLoader().discover("test", pattern="test*.py", top_level_dir=None)
2)使用 TextTestRunner 类中的 run() 方法运行获取到的测试用例。
runner = unittest.TextTestRunner()
runner.run(testcases)
3.6.7 verbosity 参数设置
在 unittest 测试框架中可以使用 main() 方法、TestRunner 类或使用命令行的方式运行测试,运行测试后可以在 VS Code 的 OUTPUT 视窗中查看运行结果。unittest 提供了一个 verbosity 参数,通过设置该参数可以展示不同详细程度的测试结果内容。
verbosity 参数有三种模式:
- 0(静默模式):测试执行结果中只会显示测试执行结果总览,即执行的总用例数及汇总结果;
- 1(默认模式):测试执行结果的内容与静默模式类似,只是增加了使用 “ . ” 、“ F ” 等简单的标识标记每个用例执行结果的内容;
- 2(详细模式):测试执行结果中除了显示执行结果总览信息,还会显示每个测试用例所有相关的执行信息。
3.7 Unittest 参数化
3.7.1 使用 ddt 方式进行参数化
在 unittest 中可以使用 ddt 的方式进行参数化,参数类型支持元组、列表和字典。参数化主要包括以下几个步骤:
- 测试类使用
@ddt
类装饰器进行修饰;
from ddt import ddt, data, unpack
- 使用
@data
方法装饰器来设定待测参数;
使用
@data
方法装饰器来设定待测参数:可以设定单组参数,也可以设定多组参数,每组参数用 () 括起来,多组参数之间用逗号分隔;每组参数中的数据与测试方法中定义的形参个数及顺序一一对应;参数的数据类型可以定义为元组、列表和字典,本例中使用的是元组:
@data((2024, "闰年"), (2023, "非闰年"), (2100, "非闰年"), (2000, "闰年"))
- 使用
@unpack
方法装饰器拆分参数数据,并将拆分出来的参数数据作为测试方法的入参。
测试方法使用
@unpack
方法装饰器进行修饰,用来拆分参数数据,支持拆分的数据类型有元组、列表和字典等。拆分后,将拆分出来的数据作为测试方法 test_leapyear() 的两个入参 inputData 和 ExpectedResult ,即测试用例的输入数据和预期输出。测试方法中,使用输入数据调用被测试方法 leapYear() 获得程序的实际输出,再使用 assertEqual 断言比较程序实际输出与测试用例预期输出是否一致,从而判断测试结果是否为通过。
@unpack
def test_leapyear(self, inputData, ExpectedResult):
res = leapYear(inputData)
self.assertEqual(res, ExpectedResult)
3.7.2 使用 json 或 yaml 文件进行参数化
unittest 中可以使用列表、元组和字典的方式构造测试数据达到数据驱动测试的目的。在实际项目的单元测试中,测试用例数据往往非常多,在代码中构造测试数据会有很多局限性。unittest 中也提供了使用外部文件进行参数化的方法,其中,json 和 yaml 格式的文件可以直接进行加载使用,不需要编写额外的读取文件的代码,非常方便。
- 使用 json 文件进行 ddt 参数化测试
json( JavaScript Object Notation )是一种轻量级的数据交换格式,它具有简洁、易于阅读和编写、易于机器解析和生成等特点。
{
"leapYear1": {"year":2024, "expectedResult": "闰年"},
"leapYear2": {"year":2023, "expectedResult": "非闰年"},
"leapYear3": {"year":2100, "expectedResult": "非闰年"},
"leapYear4": {"year":2000, "expectedResult": "闰年"}
}
import unittest
from ddt import ddt, file_data
# 待测试方法(计算闰年)
def leapYear(year):
result = ""
if year % 100 == 0:
if year % 400 == 0:
result = "闰年"
else:
result = "非闰年"
elif year % 4 == 0 :
result = "闰年"
else:
result = "非闰年"
return result
# 测试类
@ddt
class LeapYearTest(unittest.TestCase):
# 定义参数
@file_data("testData.json")
# 测试方法
def test_leapyear(self, year, expectedResult):
# 调用待测试方法leapYear()获得程序的实际输出
res = leapYear(year)
# 断言实际输出与预期结果是否一致:如果一致则测试结果为“通过”;如果不一致则测试结果为“失败”
self.assertEqual(res, expectedResult)
if __name__ == "__main__":
unittest.main()
- 使用 yaml 文件进行 ddt 参数化测试
yaml 是一种可读性高、以数据为中心的标记语言,在很多项目的测试过程中会使用这种格式的文件来作为参数化文件。
yaml 的语法规则:
- 使用 key: value 键值对的方式组织数据,冒号后面需要加一个空格;
- 使用缩进表示层级关系。缩进不允许使用 Tab ,只允许使用空格,缩进的空格数没有严格的要求,只需要将相同层级的元素左对齐即可;
- 大小写敏感;
- 使用 “ - ” 加一个空格表示一个数组项;
- ” # “ 开头的行表示注释。
testcases1:
-
year: 2024
expectedResult: "闰年"
testcases2:
-
year: 2023
expectedResult: "非闰年"
testcases3:
-
year: 2100
expectedResult: "非闰年"
testcases4:
-
year: 2000
expectedResult: "闰年"
import unittest
from ddt import ddt, file_data
# 待测试方法(计算闰年)
def leapYear(year):
result = ""
if year % 100 == 0:
if year % 400 == 0:
result = "闰年"
else:
result = "非闰年"
elif year % 4 == 0 :
result = "闰年"
else:
result = "非闰年"
return result
# 测试类
@ddt
class LeapYearTest(unittest.TestCase):
# 定义参数
@file_data("testData.yaml")
# 测试方法
def test_leapyear(self, testdata):
# 调用待测试方法leapYear()获得程序的实际输出
res = leapYear(testdata[0]["year"])
# 断言实际输出与预期结果是否一致:如果一致则测试结果为“通过”;如果不一致则测试结果为“失败”
self.assertEqual(res, testdata[0]["expectedResult"])
if __name__ == "__main__":
unittest.main()
3.7.3 使用 txt / excel / csv 文件进行参数化
在 unittest 测试框架中可以使用外部文件对测试用例数据进行参数化实现数据驱动测试。除了 json 和 yaml 文件可以直接加载作为参数化的数据来源,其它格式的外部文件都需要编写相应的方法读取并返回文件内容,再使用返回的内容作为参数化的数据来源。
- 使用 txt 文件进行参数化测试
txt 是一种常见的文本格式,在 unittest 中使用 txt 文件作为参数化文件时需要先读取文件内容,再使用 ddt 的方式进行参数化。
import unittest
from ddt import ddt, data, unpack
# 待测试方法(计算闰年)
def leapYear(year):
result = ""
if year % 100 == 0:
if year % 400 == 0:
result = "闰年"
else:
result = "非闰年"
elif year % 4 == 0 :
result = "闰年"
else:
result = "非闰年"
return result
# 读取 txt 文件的方法
def readTxtFile(file_name):
# 定义list变量
testData = []
# 打开txt文件逐行读取文件中的内容,并按分隔符进行拆分
file = open(file_name, "r", encoding="utf-8")
for line in file.readlines():
testData.append(line.strip("\n").split(","))
return testData
# 测试类
@ddt
class LeapYearTest(unittest.TestCase):
# 定义参数
@data(*readTxtFile("testdata.txt"))
# 测试方法
@unpack
def test_leapyear(self, year, expectedResult):
#调用待测试方法leapYear()获得程序的实际输出
res = leapYear(int(year))
#断言实际输出与预期结果是否一致:如果一致则测试结果为“通过”;如果不一致则测试结果为“失败”
self.assertEqual(res, expectedResult)
if __name__ == "__main__":
unittest.main()
- 使用 excel 文件进行参数化测试
编写一个读取 excel 文件的方法读取并返回文件中的测试用例数据,再将该返回值作为参数化的数据来源。
import unittest
from ddt import ddt, data, unpack
import xlrd
# 待测试方法(计算闰年)
def leapYear(year):
result = ""
if year % 100 == 0:
if year % 400 == 0:
result = "闰年"
else:
result = "非闰年"
elif year % 4 == 0 :
result = "闰年"
else:
result = "非闰年"
return result
# 读取 excel 文件的方法
def readExcelFile(file_name):
# 定义list变量
testData = []
# 打开excel文件,读取并返回其中的内容
workbook = xlrd.open_workbook(file_name)
sheet = workbook.sheet_by_index(0)
for i in range(0, sheet.nrows):
testData.append(list(sheet.row_values(i, 0, sheet.ncols)))
return testData
# 测试类
@ddt
class LeapYearTest(unittest.TestCase):
# 定义参数
@data(*readExcelFile("testdata.xls"))
# 测试方法
@unpack
def test_leapyear(self, year, expectedResult):
#调用待测试方法leapYear()获得程序的实际输出
res = leapYear(int(year))
#断言实际输出与预期结果是否一致:如果一致则测试结果为“通过”;如果不一致则测试结果为“失败”
self.assertEqual(res, expectedResult)
if __name__ == "__main__":
unittest.main()
- 使用 csv 文件进行参数化测试
使用 csv 文件进行参数化的方法与 txt 和 excel 都类似,只需要编写一个读取 csv 文件的方法返回测试数据集作为测试方法的参数化数据来源。
import unittest
from ddt import ddt, data, unpack
import csv
# 待测试方法(计算闰年)
def leapYear(year):
result = ""
if year % 100 == 0:
if year % 400 == 0:
result = "闰年"
else:
result = "非闰年"
elif year % 4 == 0 :
result = "闰年"
else:
result = "非闰年"
return result
# 读取csv文件的方法
def readCsvFile(file_name):
# 定义list变量
testData = []
# 打开csv文件,循环读取并返回读取的测试数据集
with open(file_name, "r", encoding="utf-8") as file:
datas = csv.reader(file)
for data in datas:
testData.append(data)
return testData
# 测试类
@ddt
class LeapYearTest(unittest.TestCase):
# 定义参数
@data(*readCsvFile("testdata.csv"))
# 测试方法
@unpack
def test_leapyear(self, year, expectedResult):
#调用待测试方法leapYear()获得程序的实际输出
res = leapYear(int(year))
#断言实际输出与预期结果是否一致:如果一致则测试结果为“通过”;如果不一致则测试结果为“失败”
self.assertEqual(res, expectedResult)
if __name__ == "__main__":
unittest.main()