读后笔记 -- Python 全栈测试开发 Chapter4:自动化测试框架:unittest

4.1 自动化测试分层思想

1. 框架的设计过程

  • 1)线性脚本(Selenium API):很明确地体现整个的场景走向,=> 但完全不适用于实际引用
  • 2)结构化脚本(Selenium API + Python 面向对象): 包括两种类型:模块脚本、库脚本;不同的业务场景设计在不同的模块中;不同的功能点以库进行分类;=> 完全不适用于实际引用(代码重复率高,难维护)
  • 3)数据驱动脚本:数据与脚本实现分离;只考虑业务流的操作过程,不考虑每步业务操作所需要定位元素的相关信息(直接封装在底层);
  •                                数据分离的实现方式太多(数据存储的文件格式、数据参数化的方式)
  • 4)关键字驱动(Selenium IDE):将每步业务操作封装成每个关键字,每个业务流操作封装成关键字对象;后期只需要调用每个关键字即可完成整个业务场景的实现()

2. 自动化分层思想:

  不同的程序设计者、公司其分层的思想、框架略有不同,但整体的核心分层方式大概如下:

  • 1)页面元素处理层:Page Object(PO 模式)页面对象管理,将每个页面上的所有元素定义在一个模块(即.py)中,方便后期维护(如果前端不变,则不用更新,即实现了底层封装)
  • 2)业务流层:基于页面元素处理层实现业务流的自由组织,(实际中,该层可融入到测试用例层)
  • 3)测试用例层:根据业务流场景设计相应的测试用例并执行。通常使用自动化测试框架(unittest、pytest),实践表明在整个脚本中用例管理使用单元自动化测试框架是最佳的管理方式(用例的组织、设计、管理)
  • 4)数据分离层:将数据提取出来进行专门的数据模块管理,后期可独立维护,不用考虑底层代码;
  • 5)公共层:报告生成、邮件发送、日志生成及分析等,实际中,可将 数据分离层也放在 公共层;
  • 6)主程序入口层:可独立,也可放在公共层;

    使用完自动化测试框架后,要修改数据找数据文件,要修改操作只需找业务流层

 


4.2. 单元测试框架

unittest 框架

1、单元测试框架:不同语言的单元测试框架不同,所有的单元测试框架都从 java 语言的 junit(自动实现预期与实际比较---断言)

2、unittest 框架(python 内置模块),核心内容主要包括:

 a. test fixture 测试固件:实现整个测试中所有用例所需要资源的管理操作;资源(文件里、数据库资源、驱动器对象)的创建、销毁等;主要由:setUp 和 tearDown,且方法名固定

  • setUp:每个测试用例执行前将所需要的对象进行全新初始化;
  • teadDown:每个测试用例执行后将涉及到的所有对象销毁;

    ** 整个 fixture 中的执行顺序是: setUpClass -> setUp -> testcase1 -> tearDown -> setUp -> testcase2 -> tearDown -> .... -> tearDownClass

 b. test case 测试用例:使用 unittest 框架时,测试类必须继承 TestCase,并且 测试方法的名称以“test” 开头

 c. test suite 测试套件:一组测试用例的集合;套件可实现用例以模块化形式执行,以及以测试类的形式 自定义执行哪些用例;

 d. test runner 测试运行器

  • unittest 框架运行器:class LoginFlowTest(unittest.TestCase) -> unittest.main()
  • 参数运行器:class LoginFlowTestParamunittest(paramunittest.ParameterizedTestCase) -> unittest.main()
  • 套件运行器:def test_all() -> suite = unittest.Testsuite(), load = unittest.TestLoad(), suite.addTest(), runner = unittest.TextTestRunner(), runner.run(suite) -> test_all()
特别说明:
1. 执行方式:
  1)光标在测试方法前:执行该测试方法
  2)
光标在测试类前:执行测试类   3)创建 __main__ 方法,执行 unittest.main():执行测试类
2. unittest 框架中用例实现的重点是 断言,
  一般断言有三个参数:预期值,实际结果, 断言失败的提示,如 assertEqual(first, second, msg=None)
  通过查看 assertEquals 源码即可知,其就是 assertEqual 的早期实现,现在都专用 assertEqual 了。其他的 xxxs 通用。

3. unittest 框架中,运行结果状态主要有三种:
  1)errors:如 value1 = 1/0 => 单元测试代码有问题
  2)failures:断言失败    => 预期与实际不一致,可推断出源代码内部逻辑有问题
  3)passed:断言成功
4. unittest 框架中的 test case 是以 名称的 ASCII 码值为顺序执行(即 [0-9], [A-Z],[a-z]),
模块命名会针对被测试模块的名称前面或后面加 Test

3、unittest 框架的受限

  • 不能通过一个用例完成多条数据的测试(也就是说不能通过循环的方式处理,场景:测试多个有效账户的登录)=> 借助第三方的 参数化
  • 无报告生成方式

 


4.2.4/5 参数化

1. parameterized (支持:nose、unittest、pytest 框架)=> 应用最广  

说明:

  • 1)可实现 python 中的多个测试框架的参数,如 nose、unittest、pytest;
  • 2)可实现类的参数化,也可实现函数的参数化(仅支持在 nose 和 pytest 框架);
  • 3)parameterized_class 如装饰在类上,那么装饰的入参有两种形式:
  •        a)直接传入一个列表类型的字典值 => ([{key1.1:value1.1, key1.2:value1.2}, {key2.1:value2.1, key2.2:value2.2}, ...])
  •        b)传入一个元组,对应的值是一个二维序列(元组、列表)=> ((tuple),[(tuple value1), (tuple value2), ..]);
  • 4)parameterized.expand 装饰在方法上,且方法中入参;传入的是一个二维序列(元组、列表)    

 

2. paramunittest(必须在 unittest 框架下) => 应用相对较少

# 1. pip install paramunittest

# 2. 测试脚本
import unittest
from Business_Flow_Layer.Login_Flow import LoginFlow
from Data_Process_Layer.DataBase_Data.DataBase_Login import DataBaseLogin
from Public_Layer.MD5_Encrypt_Data import get_md5_text
from Business_Flow_Layer.HomePage_Flow import HomePageFlow
import paramunittest


@paramunittest.parametrized(("admin", "2016"), ("user1", "123456"), ("user2", "123456"))
class LoginFlowTestMultipleUsers(paramunittest.ParametrizedTestCase):

    def setParameters(self, username, password):
        self.username = username
        self.password = password

    # 不加这句话, tearDownClass 中的 cls.database_login 会黄色警告,提示"Unresolved attribute reference 'database_login for class 'LoginFlowTest'"
    # 原因是:其在解释器时(还未执行代码时),LoginFlowTest 检测不到当前类的属性 database_login 在 setUpClass 中不存在该问题,其在一开始就执行,该对象必先创建
    database_login = None

    @classmethod
    def setUpClass(cls):
        cls.database_login = DataBaseLogin()

    @classmethod
    def tearDownClass(cls):
        cls.database_login.close_database()

    def setUp(self):
        self.login_flow = LoginFlow("https://quantpt.gecenet.com/manage/index.html#/login/login", "chrome")

    # 5. 用户名、密码正确
    def test_username_pwd_correct(self):
        # 使用入参的用户名、密码
        self.login_flow.login(self.username, self.password)
        # database_login = DataBaseLogin()
        self.assertIsNotNone(self.database_login.get_db_uname(self.username), "数据库中无该用户名 %s 存在" % self.username)
        self.assertEqual(get_md5_text(self.password), self.database_login.get_db_upassword(self.username))
        # 此处传入登录成功后的对象 get_element 给后续的方法使用
        # 目的:不用再创建驱动器对象 get_element,而不是具体的值 get_element()
        self.assertEqual(self.username, HomePageFlow().get_userinfo_text(self.login_flow.get_element))

    def tearDown(self):
        self.login_flow.get_driver.quit()


if __name__ == '__main__':
    unittest.main()

说明:

  • 1)必须先定义 setParameters 方法且方法名固定,否则 paramunittest 找不到提供给测试单元的传参入口;
  • 2)@paramunittest.parametrized 支持的参数类型包括:元组、列表、字典;
  • 3)装饰入参,如果采用字典形式,则键名须 setParameters 的参数名一致。即 @paramunittest.parametrized({"username" : "admin" "password" : "2016"}),则在 setParamters 方法中参数名需要时 username,password,否则找不到键值
  • 4)使用了 paramunittest 后,针对每个 test case,参数列表的每个参数都会运行一次,如 5个 test case,给出3个参数,则总共运行 3 * 5 次

总结:主要针对的是 unittest 框架实现的参数化,并且装饰在类上,不能单独针对测试方法进行参数化;

 

3. DDT(Data Driver Test,数据驱动测试)=> 应用很少

ddt 用例:https://www.cnblogs.com/bruce-he/p/17566994.html 

【这三种方式,不是真正意义上的 数据驱动测试,仅是数据驱动测试的数据参数化。】

可以实现多种参数类型的数据结构传入

  • 1)单一数据结构:无论传入的数据是何种类型,都当一个值处理;=> 方法的参数就1个,然后需要对参数进行拆解
  • 2)复合数据结构类型处理(list、tuple、dict):如需对数据分别对应参数,则加 @ddt.unpack。@unpack 可以放 @data 前面,但一般建议放 @data 后面
  • 3)可以读取数据格式文件中的数据进行参数化;

 

4 数据格式文件的操作

可实现测试数据与测试用例进行分离;

处理数据的文件操作包含:csv、yaml、excel、json、ini、数据库处理等,web 中常用的是 csv 和 yaml

 

 5. 总结:测试用例的添加方法

方式一:addTest()、addTests() 添加用例方法

suite = unittest.TestSuite()
suite.addTest(testcase1)
suite.addTests([testcase1, testcase2])

runner = unittest.TextTestRunner()
runner.run(suite)
# 1)普通类(可行):参数: 类(方法名)
suite.addTest(LoginFlowTestSimple("test_username_is_null"))

# 2)普通类(可行):参数:[类(方法名), 类(方法名)]
suite.addTests((LoginFlowTestSimple("test_username_not_exist"), 
                       LoginFlowTestSimple("test_user_is_forbidden")))
3)参数类(不可行):不能通过 suite.addTest() 方法添加参数化类的某个测试方法,"no such test method"
suite.addTest(LoginFlowTestIntegratedParamterized("test_login_integrated_parameterized"))
 方法二:TestLoader() 
suite = unittest.TestSuite()
loader = unittest.TestLoader()

suite.addTest()

runner = unittest.TextTestRunner()
runner.run(suite)
  
1.1)普通类(可行):参数:类名
suite.addTest(self.load.loadTestsFromTestCase(LoginFlowTestSimple))

1.2)参数类(不可行):参数:类名 => 打印出来的列表是空
suite.addTest(self.load.loadTestsFromTestCase(LoginFlowTestIntegratedParamterized))
2.1)普通类(可行):参数:Module 对象名
suite.addTest(self.load.loadTestsFromModule(Login_Flow_Test_Simple))

2.2)参数化类(可行):参数:Module 对象名
suite.addTest(self.load.loadTestsFromModule(Login_Flow_Test_Integrated_Parameterized)) 
3.1)参数:方法名
--- 3.1.1) 普通类:项目下的包1.包2...ModuleName.ClassName.方法名
suite.addTest(self.load.loadTestsFromName("Test_Case_Layer.OtherMethods.Login_Flow_Test_Simple.LoginFlowTestSimple"
".test_username_is_null
")) --- 3.1.2) 方法上放置参数来参数化用例方法的指定数据的测试 => 测试用例方法名+索引+所有参数 suite.addTest(self.load.loadTestsFromName("Test_Case_Layer.OtherMethods.Login_Flow_Test_Integrated_DDT"
".LoginFlowTestIntegratedDDT"
".test_login_fail_5___wll____123456____账号已禁用__
")) --- 3.1.3) yaml 参数化的指定数据的测试 => 测试用例方法名+索引+所有参数 suite.addTest(self.load.loadTestsFromName("Test_Case_Layer.Login_Flow_Test.LoginFlowTestIntegratedParamterizedYaml"
".test_login_fail_3_wangchen
")) 3.2)参数: 模块名 ---- 3.2.1) 普通类 suite.addTest(self.load.loadTestsFromName("Test_Case_Layer.OtherMethods.Login_Flow_Test_Simple")) ---- 3.2.2)参数化类 suite.addTest(self.load.loadTestsFromName("Test_Case_Layer.OtherMethods.Login_Flow_Test_Integrated_DDT")) 3.3)参数:多个模块化名 suite.addTest(self.load.loadTestsFromNames(["Test_Case_Layer.OtherMethods.Login_Flow_Test_Simple",
"Test_Case_Layer.HomePage_Flow_Test"]))

    方法三: discover 

suite = unittest.TestSuite()
loader = unittest.TestLoader()
testcases_dir = GET_PATH + "\Test_Case_Layer"

suite.addTest(loader.discover(testcases_dir, pattern="*_Test_*.py"))

runner = unittest.TextTestRunner()
runner.run(suite)
 
suite.addTest(loader.discover(testcases_dir, pattern="*_Test_*.py"))
# 打印所有的测试用例名称,特别注意 参数化的方法名
# print(loader.discover(testcases_dir, pattern="*Login_Flow_Test.py"))

 


4.3 unittest 扩展

1. HTMLTestRunner 生成报告

参照 https://www.cnblogs.com/bruce-he/p/16778896.html

HTMLTestRunner 是基于 unittest 框架实现的一个简单的 html 格式的报告

 

2. email 发送邮件

参照:https://www.cnblogs.com/bruce-he/p/16782736.html 

 

3. 日志

参照:https://www.cnblogs.com/bruce-he/p/16785389.html

  

posted on 2022-10-01 17:40  bruce_he  阅读(82)  评论(0编辑  收藏  举报