测试学习记录,仅供参考!
Pytest框架钩子函数
钩子函数在 pytest 框架中是一个比较重要的概念;在测试执行前、或测试执行过程中在前置功能里面做一些操作;可以自定义钩子函数;
一、钩子函数
在Pytest框架中,钩子函数是一种允许用户扩展或自定义测试执行过程的机制,钩子函数允许用户在测试的不同阶段插入自定义的代码,以实现特定的行为、操作或处理;这种插入式的机制使得Pytest具有高度的灵活性和可扩展性;
Pytest的钩子函数遵循一定的命名规则,通常以"pytest_"为前缀。用户可以在测试代码中定义这些函数,Pytest会在特定的时机调用它们;
比较常用的Pytest钩子函数有以下几种(已在 pytest 框架中预定义的是只有去使用,不能改函数名):
1、pytest_configure(config)
在配置阶段调用,用于设置全局配置,可以配置插件、自定义命令行选项等;
某些情况下会遇到(偶尔);
2、pytest_collection_modifyitems(config, items)
在收集测试用例阶段调用,允许修改、过滤或重新排序测试用例;
可以修改测试用例运行的排序规则;有时候会用到;
3、pytest_runtest_protocol(item, nextitem)
在测试用例执行的不同阶段调用,可以用于在测试前后执行特定操作;
用的不是很多,基本上不使用;
4、pytest_fixture_setup(fixturedef, request)
在每个fixture设置之前调用,可以用于自定义fixture的行为;
偶尔使用;
5、pytest_fixture_post_finalizer(fixturedef, request)
在每个fixture的最终清理阶段调用,用于自定义fixture的清理操作;
若已经有在代码中自定义后置操作清理数据功能,可以不再使用此钩子函数;
6、pytest_runtest_makereport(item, call, report)
在每个测试用例运行结束后调用,允许自定义测试报告;一般在conftest.py 中使用;
需要用到,例如:执行测试用例失败截图,最终跑完之后只有测试失败的才会截图,正常通过的不截图;
7、pytest_terminal_summary(terminalreporter, exitstatus, config)
在测试执行完毕后,用于生成并显示最终的测试摘要信息到终端,你可以实现自定义的测试报告汇总和显示。例如,可以在此钩子函数中计算测试覆盖率、输出额外的统计信息等等;用的比较多,在conftest.py文件使用,所有测试用例执行完毕后自动收集测试结果;
二、实际运用
1、一般钩子函数均需要写到 conftest.py 配置文件中;
2、这里选择把钩子函数写到 项目根目录 testcase 软件包下 conftest.py 文件中(自行选择);
pytest_runtest_makerepor
3、以 pytest_runtest_makerepor 为例:
1)、定义一个 def pytest_runtest_makereport() 函数,因为是预定义的,所以不能修改其函数名;
2)、@pytest.hookimpl(hookwrapper=True):添加 @pytest 装饰器标记此函数是一个钩子函数;
3)、开始写代码实现;写一个 yield(yield上面的是前置操作,下面的是后置操作)
4)、coucome = yield :自定义一个变量,把结果给返回出去;
5)、通过这个 coucome 结果去调用 .get_result() 获取它的结果,返回出去;再打印查看;
# 钩子函数 @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(): coucome = yield result = coucome.get_result() print(result)4、单独运行某条测试用例(自行设置);运行完之后可以发现每条测试用例有三个阶段;
when='setup' 运行前
when='call' 运行过程中
when='teardown' 运行结束后
能够在终端控制台打印出每条测试用例三个阶段“执行前、执行中、执行结束后”的信息结果,通过 get_result() 方法去获取测试结果的报告对象,这个报告对象包含测试用例的各种信息,例如:测试用例名称、阶段、执行结果;
- ------测试用例开始执行testcase------ <TestReport 'testcase/other/test_file_upload.py::TestFilesUpload::test_file_upload_success' when='setup' outcome='passed'> ...... <TestReport 'testcase/other/test_file_upload.py::TestFilesUpload::test_file_upload_success' when='call' outcome='passed'> PASSED<TestReport 'testcase/other/test_file_upload.py::TestFilesUpload::test_file_upload_success' when='teardown' outcome='passed'> ------测试用例执行完毕testcase------5、拿到每个测试结果的报告对象,对报告对象进一步操作;
1)、if result.when == 'call': 判断一下这个结果的阶段是“执行中”阶段;通过 result 去调用执行阶段的属性 when 来判断属于哪个阶段;
2)、xfail = hasattr(result, 'wasxfail'):判断测试结果里面是不是包含了自定义 wasxfail 属性;返回结果;
hasattr(result, 'wasxfail'):使用 hasattr 方法检测这个 result 对象有没有这个 wasxfail 属性;其实就是用于标识测试用例是否预期失败的作用;
3)、if (result.skipped and xfail) or (result.failed and not xfail): 进一步判断,这里因为要判断两个,所以使用小括号 () ;再判断 如果是一个跳过的 或者 是一个失败的 测试用例;
(result.skipped and xfail):判断测试用例是否被跳过并且是预期失败;
(result.failed and not xfail):判断测试用例是否执行失败并且不是预期失败;
4)、with:测试用例执行失败时要做的一系列操作;
5)、with allure.step('测试用例失败截图'): 引入 allure 模块,使用 allure 报告中的 step 方法(这个方法表示在测试报告中创建一个步骤),测试步骤打印一句话(自定义),然后在步骤下面去创建一个截图;
6)、用例截图,可以发现截图方法之前封装在公共方法里面的,使用self.get_screenshot_as_png()截图方法需要先引入公共方法模块,再传浏览器对象,但是此时 conftest.py 这个文件中已经有初始化浏览器对象,直接调用即可;但是调用的话,不能直接通过调用方法函数的方式,它会找不到前置对象,需要把浏览器初始化对象设置为全局属性(自行选择合适的初始化浏览器对象),在通过这个全局属性去调用driver.get_screenshot_as_png()截图方法;
allure.attach(self.get_screenshot_as_png(), '失败截图', attachment_type=allure.attachment_type.PNG)
allure.attach(driver.get_screenshot_as_png(), '失败截图', attachment_type=allure.attachment_type.PNG)
7)、失败测试用例可以做其他操作,不一定非要截图,例如:失败重跑等等一系列其他的操作;
import pytest from selenium import webdriver from other.selenium_demo import driver from util_tools.logs_util.recordlog import logs from config.setting import browser_type, WAIT_TIME from pageObject.login_page.login_page import LoginPage import allure @pytest.fixture(autouse=True) def log_outputs(): logs.info('------测试用例开始执行testcase------') yield logs.info('------测试用例执行完毕testcase------') # @pytest.fixture() # def get_driver(): # # 将driver设置为全局变量 # global driver # # # 初始化浏览器对象--字典 # browser_mapping = { # 'Chrome': webdriver.Chrome, # 'Edge': webdriver.Edge, # 'Firefox': webdriver.Firefox # } # # # 判断配置文件 browser_type 变量值包含在 browser_mapping 字典里面 # if browser_type.capitalize() in browser_mapping: # driver = browser_mapping.get(browser_type.capitalize())() # # # 设置一个全局的隐式等待时间 # driver.implicitly_wait(WAIT_TIME) # # 最大化浏览器窗口 # driver.maximize_window() # yield driver # driver.quit() def init_driver(): # 初始化浏览器对象--字典 browser_mapping = { 'Chrome': webdriver.Chrome, 'Edge': webdriver.Edge, 'Firefox': webdriver.Firefox } # 判断配置文件 browser_type 变量值包含在 browser_mapping 字典里面 if browser_type.capitalize() in browser_mapping: return browser_mapping.get(browser_type.capitalize())() @pytest.fixture(scope='class') def get_driver(): driver = init_driver() # 设置一个全局的隐式等待时间 driver.implicitly_wait(WAIT_TIME) # 最大化浏览器窗口 driver.maximize_window() yield driver driver.quit() # 登录状态的前置应用 @pytest.fixture(scope='class') def login_driver(get_driver): driver = get_driver login_page = LoginPage(driver) # 自行设置是否参数化 login_page.login('admin123', '123456') return driver # 未登录状态的前置应用 @pytest.fixture() def not_login_driver(): # 把浏览器初始化对象设置为全局属性driver global driver driver = init_driver() # 设置一个全局的隐式等待时间 driver.implicitly_wait(WAIT_TIME) # 最大化浏览器窗口 driver.maximize_window() yield driver driver.quit() # 钩子函数,对失败测试用例进行截图 @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(): coucome = yield result = coucome.get_result() # 判断这个阶段是“执行中”阶段 if result.when == 'call': # xfail = hasattr(result, 'wasxfail') # 再判断 如果是一个跳过的 或者 是一个失败的 测试用例 if (result.skipped and xfail) or (result.failed and not xfail): # 失败时要做的操作--用例截图 with allure.step('测试用例失败截图'): # 设置浏览器初始化对象为全局属性driver,通过driver去调用get_screenshot_as_png()方法 allure.attach(driver.get_screenshot_as_png(), '失败截图', attachment_type=allure.attachment_type.PNG)6、可自行设置失败的测试用例去查看结果;
pytest_terminal_summary(terminalreporter, exitstatus, config)
7、这次在项目根目录下 conftest.py 文件中使用(自行选择);
1)、 简单示例如下:把所有测试用例收集完之后才去统计,相当于前后置操作中的后置处理;
def pytest_terminal_summary(terminalreporter, exitstatus, config): """ pytest预定义钩子函数,用于自动收集测试用例执行结果 :param terminalreporter: 报告汇总,内部使用的终端测试报告器对象,用于输出内容 :param exitstatus: 返回码退出状态:0-用例全部通过;1-有用例失败;2-还没执行,在收集用例就失败了;3、4-其他报错;5-收集到0条用例 :param config: pytest全局config配置对象 :return: """ # 调用里面的内置方法,最后把收集统计到的测试用例总数量返回出去给一个变量(把所有测试用例收集完之后才去统计) total = terminalreporter._numcollected # 收集测试结果 passeds = terminalreporter.stats print(f'测试结果:{passeds}') print(f'统计测试用例总数:{total}') print(f'返回码退出状态:{exitstatus}') print(f'全局config对象:{config}')2)、自行运行测试用例查看控制台输出结果:
测试结果:最外层是通过一个字典去存储所有的测试用例,里面以键值对的展示;无论是成功的还是失败的值均是以列表类型 统计测试用例总数:XXXX 返回码退出状态:0 全局config对象:<_pytest.config.Config object at 0x000001F5BC2175E0>8、优化项目根目录下 conftest.py 文件(可自行设置查看其他效果,例如:测试失败,等待超时跳过等等);
# 导包 import time import pytest from util_tools.connectMysql import ConnectMysql from util_tools.logs_util.recordlog import logs # 添加后置应用--数据清理 @pytest.fixture(scope='session', autouse=True) def data_cleaning(): """测试结束后清理测试数据""" # 初始化连接数据库对象 conn = ConnectMysql() yield logs.info('正在清理测试数据...') # 删除语句(注册成功用户)--若有别的可以继续写 sql = 'delete from ecs_users where user_name="test06"' # 调用删除语句 conn.delete(sql) def pytest_terminal_summary(terminalreporter, exitstatus, config): """ pytest预定义钩子函数,用于自动收集测试用例执行结果 :param terminalreporter: 报告汇总,内部使用的终端测试报告器对象,用于输出内容 :param exitstatus: 返回码退出状态:0-用例全部通过;1-有用例失败;2-还没执行,在收集用例就失败了;3、4-其他报错;5-收集到0条用例 :param config: pytest全局config配置对象 :return: """ # 调用里面的内置方法,最后把收集统计到的测试用例总数量返回出去给一个变量(把所有测试用例收集完之后才去统计) total = terminalreporter._numcollected # 通过数 passed = len(terminalreporter.stats.get('passed', [])) # 失败数 failed = len(terminalreporter.stats.get('failed', [])) # 错误数 error = len(terminalreporter.stats.get('error', [])) # 跳过数 skipped = len(terminalreporter.stats.get('skipped', [])) # 执行总时长--调用内置函数round() duration = round(time.time() - terminalreporter._sessionstarttime, 2) # 定义一个字符串 summary = f""" 自动化测试结果,通过如下,具体执行结果如下: 测试用例总合计数:{total} 测试执行通过数:{passed} 测试执行失败数:{failed} 错误数量:{error} 跳过执行测试用例数量:{skipped} 执行测试用例总耗时:{duration}秒 """ # 打印字符串 print(summary)9、运行主函数 run.py 文件(执行所有的测试用例);可以发现执行的稍慢,是因为设置了 10 秒的隐式等待(全局等待),当某一个元素没有找到的情况下,会去等 10 秒钟,所以导致时间很慢,可根据实际场景去设置小一点的隐式等待时间;
自动化测试结果,通过如下,具体执行结果如下: 测试用例总合计数:12 测试执行通过数:12 测试执行失败数:0 错误数量:0 跳过执行测试用例数量:0 执行测试用例总耗时:219.02秒 ======================= 12 passed in 219.02s (0:03:39) ========================未完待续。。。