pytest的fixture的详细使用 (更灵活高级的前/后置处理方法)
一、fixture基本操作介绍
虽然pytest在unittest的两组前置后置方法方法基础上,提供了更全面的总共五组的前置后置方法,但这些方法都是针对各自对应的整个作用域全局生效的,
如果有以下场景:用例 1 需要先登录,用例 2 不需要登录,用例 3 需要先登录。很显然无法用 setup 和 teardown 来实现了
pytest框架的精髓fixture可以让我们随心所欲的定制测试用例的前置后置方法
fixture是pytest将测试前后进行预备、清理工作的代码分离出核心测试逻辑的一种机制
1.基本形式和用法:
@pytest.fixture() 装饰器用于声明函数是一个fixture,该fixture的名字默认为函数名,也可以自己指定名称(详见参数name解释)
如果测试用例的参数列表中包含fixture的名字,那么pytest会根据名字检测到该fixture,并在测试函数运行之前执行该fixture
fixture可以完成测试任务,也可以返回数据给测试用例
import pytest @pytest.fixture(scope="function", params=None, autouse=False, ids=None, name=None) def func_01(): print("这就定义了一个简单的fixture") return '结果' def test_01(func_01): print(type(func_01), func_01) # <class 'str'> 结果 if __name__ == '__main__': pytest.main()
2.检测顺序:
检测顺序是:当前测试类 > 模块(.py文件)> 当前包中conftest.py > 父包中conftest.py > 根目录中conftest.py
3.存放位置
- 可以放在测试用例自己的测试文件里
- 如果希望多个测试文件共享fixture,可以放在某个公共目录下新建一个conftest.py 点击查看,将fixture放在里面
4.fixture调用方式
注意事项: 如果不是测试用例,无论哪种调用方式都不会生效(参考func_006) 未传入fixture,不会执行仍何fixture
1)参数传参:
将fixture函数名当参数传入用例(函数名无引号)
支持多个,支持fixture相互调用时给fixture传参
返回值:fixture执行完毕将返回值赋值给用例参数名,无返回值默认为None
2)装饰器传参:
支持多个,不支持fixture相互调用时给fixture传参
返回值:不能获取
第一种:传入名字,@pytest.mark.usefixtures("fixture1", "fixture2") (字符串格式,带引号的)
第二种:多个可以使用@pytest.mark.usefixture()进行叠加,先执行的放底层,后执行的放上层
3)autouse=True,自动调用,详见下一节: 其他参数使用
5.fixture实例化顺序
- 高级别scope的fixture在低级别scope的fixture之前实例化(session > package > module > class > function)
- 具有相同scope的fixture遵循测试函数中声明的顺序
- 遵循fixture之间的依赖关系【在fixture_A里面依赖的fixture_B优先实例化,然后到fixture_A实例化】
- (autouse=True)自动使用的fixture将在显式使用(传参或装饰器)的fixture之前实例化
上面的规则是基本前提,当遇到不同级别fixture相互调用的情况时,实例化顺序会很复杂让人头疼:(使用需谨慎)
import pytest order = [] @pytest.fixture(scope="session") def s1(): order.append("s1") @pytest.fixture(scope="session") def s2(): order.append("s2") @pytest.fixture(scope="session") def s3(): order.append("s3") @pytest.fixture(scope="session") def s4(): order.append("s4") @pytest.fixture(scope="session") def s5(s7): order.append("s5") @pytest.fixture(scope="session") def s6(): order.append("s6") @pytest.fixture(scope="session") def s7(): order.append("s7") @pytest.fixture(scope="module") def m1(): order.append("m1") @pytest.fixture(scope="module") def m2(s5): order.append("m2") @pytest.fixture(scope="module") def m3(s4): order.append("m3") @pytest.fixture def f1(s2, f3): order.append("f1") @pytest.fixture def f2(m2, s3): order.append("f2") @pytest.fixture def f3(s6): order.append("f3") def test_order(f2, f1, m3, m1, s1): print(order) # ['s1', 's3', 's2', 's4', 's7', 's5', 's6', 'm3', 'm1', 'm2', 'f2', 'f3', 'f1'] if __name__ == '__main__': pytest.main()
可以先画出继承关系图,这样就会很明了,按着等级去找就对了:
在实例化基本规则大前提下,fixture可能存在不同等级之间的相互调用,这就存在依赖深度等级,如图:
1.所有的session级必定最先执行,接下来是确定该级别fixture的先后顺序:
1)先运行第一等级的session级别,运行s1
2)第二等级按照传入(f2, f1, m3, m1, s1)传入顺序依次查找,得到 s3 > s2 > s4
3)第三等级按照传入(f2, f1, m3, m1, s1)传入顺序依次查找,得到 (s5依赖于同scope级别s7)s7 > s5 > s6
2.接下来运行module级别
1)第一等级按照传入顺序 m3 > m1
2)第二等级按照传入(f2, f1, m3, m1, s1)传入顺序依次查找,得到 m2
3.运行function级别
1)第一等级按照传入顺序 f2 > f3 > f1 (f1依赖于同scope级别f3)
走完以上流程得到最终结果: ['s1', 's3', 's2', 's4', 's7', 's5', 's6', 'm3', 'm1', 'm2', 'f2', 'f3', 'f1']
二、scope参数详解(fixture的作用范围)
fixture里面scope参数可以控制fixture的作用范围:session > module > class > function(默认)
fixture可相互调用,但要注意:如果级别不同,低级别可以调用高级别,高级别不能调用低级别
- function:每一个调用了该fixture的函数或方法执行前都会执行一次,在测试用例执行之后运行销毁代码
- class:每一个类只调用一次,一个类中可以有多个用例调用,但是只在最早运行的用例之前调用一次(每一个类外函数用例也都会调用一次)
- module:每一个.py文件调用一次,该模块内最先执行的调用了该fixture的用例执行前运行且只运行一次
- session:是多个文件调用一次,可以跨.py文件调用,每个.py文件就是module
1.scope=function 函数级(默认级别)
最基本的fixture
1.执行时机
每一个调用了该fixture的函数或方法执行前都会执行一次,在测试用例执行之后运行销毁代码
2.fixture可相互调用(参考test_002的login_and_logout)
1)fixture可以像测试用例的参数传参一样调用其他的fixture,并获取其返回值
2)多个fixture的执行顺序和测试用例调用的情况是一样的
3)只支持参数传入fixture,不支持装饰器传参
代码:
import pytest @pytest.fixture def login(): print("打开浏览器") return 'chrome' @pytest.fixture() def logout(): print("关闭浏览器") @pytest.fixture() def login_and_logout(logout, login): # fixture的相互调用,支持调用多个,执行顺序:loguot > login > login_and_logout print(f"先打开{login},又关闭了它") return f'{login} + {logout}' def test_005(login): # 第一种传参:fixture的名字作为参数传参============================================================================= print(f'005: login={login}') class TestLogin: def test_001(self, login): print(f"001: login={login}") def test_003(self, login, logout): # 支持传多个,执行顺序按照传入顺序:login > logout > test_003 print(f"003: login={login} logout={logout}") def test_002(self, login_and_logout): print(f"002: login_and_logout={login_and_logout}") def test_004(self): print("004:未传入,不会执行仍何fixture") @pytest.mark.usefixtures("login", "logout") # 第二种传参:装饰器传参,传入fixture的str,这种不能获取返回值============== def test_005(self): print(f"005: 这样传参无法获取到fixture的返回值") @pytest.mark.usefixtures("login", "logout") def func_006(self): print("006:不是测试用例,加了装饰器也不会执行fixture") if __name__ == '__main__': pytest.main()
2.scope=class 类级别
1.调用方式:
和function一样
2.运行时机:
1)类外的独立函数用例执行前会执行一次,参考test_006
2)类中有用例调用了fixture,则会在最先调用的那个用例执行前执行一次(参考test_002),该类下其他调用了fixture的用例不再执行(参考test_003)
3)未调用该fixture的类和函数,不会运行
2.fixture相互调用规则
1)类级可以调用类级,参考test_004
2)函数级可以调用类级,参考test_008
3)类级不可以调用函数级,参考test_007
代码:
import pytest @pytest.fixture(scope='class') def login(): print("打开浏览器 -- 类级别fixture") return 'chrome' @pytest.fixture() def logout(): print("关闭浏览器 -- 函数级fixture") @pytest.fixture(scope='class') def class_use_class(login): print(f"class_use_class -- 类级可以调用类级") return f'{login}' @pytest.fixture() def function_use_class(login): print(f"function_use_class -- 函数级调用类级") @pytest.fixture(scope='class') def class_use_function(logout): print(f"错误示范,类级不可以调用函数级") def test_006(login): print(f'006: login={login}') class TestLogin: def test_001(self, logout): print(f"001: login={logout} 调用普通函数级") def test_002(self, login): print(f"002:logout={login} 调用类级,该类中login只会在这里运行一次") def test_003(self, login, logout): print(f"003: login={login} logout={logout} 调用类级和函数级,该类中类级login不会再运行") @pytest.mark.usefixtures("class_use_class") def test_004(self, class_use_class): print(f"004: class_use_class 调用类级-->类再调用类级login,运行过了不会再运行") # def test_004(self, class_use_class): # print(f"004: class_use_class={class_use_class} 调用类级-->类再调用类级login,运行过了不会再运行") def test_007(self, class_use_function): print(f"007: class_use_function={class_use_function} 错误示范,类级不可以调用函数级") def test_008(self, function_use_class): print(f"008: function_use_class 调用函数级-->函数级再调用类级login,运行过了不会再运行") def test_005(self): print(f"005: 未传入任何fixture,哪个级别都与我无关") class TestNoFixture: def test_009(self): print('009:未传入任何fixture,哪个级别都与我无关') if __name__ == '__main__': pytest.main()
3.scope=module 模块级
1.调用方式:
和function一样
2.运行时机:
每一个.py文件调用一次,该模块内最先执行的调用了该fixture的用例执行前运行且只运行一次(如test_006 + test_001 + test_003)
2.fixture相互调用规则
1)类级可以调用模块级,参考test_005
2)函数级可以调用模块级,参考test_008
3)模块级不可以调用函数级,和类级参考 test_004 test_007
代码:
import pytest @pytest.fixture(scope='module') def open(): print("打开电脑 -- 模块级别fixture") return 'windows' @pytest.fixture(scope='class') def login(): print("打开浏览器 -- 类级别fixture") return 'chrome' @pytest.fixture() def logout(): print("关闭浏览器 -- 函数级fixture") @pytest.fixture(scope='module') def module_use_class(login): print(f"错误示范,模块级不可以调用类级") return f'{login}' @pytest.fixture(scope='module') def module_use_func(logout): print(f"错误示范,模块级不可以调用函数级") return f'{logout}' @pytest.fixture() def function_use_module(open): print(f"function_use_module -- 函数级调用模块级") @pytest.fixture(scope='class') def class_use_module(open): print(f"class_use_module -- 类级调用模块级") def test_006(login): print(f'006: login={login}') class TestLogin: def test_001(self, open): print(f"001: open={open} 调用模块级") def test_002(self, login): print(f"002:login={login} 调用类级") def test_003(self, open, login, logout): print(f"003: open={open} login={login} logout={logout} 调用模块级、类级和函数级") @pytest.mark.usefixtures("module_use_class") def test_004(self): print(f"004: module_use_class 错误示范,模块级不能调用类级") def test_007(self, module_use_func): print(f"007: module_use_func 错误示范,模块级不能调用函数级 ") def test_008(self, function_use_module): print(f"008: function_use_module") def test_005(self, class_use_module): print(f"005: class_use_module") class TestNoFixture: def test_009(self): print('009:未传入任何fixture,哪个级别都与我无关') if __name__ == '__main__': pytest.main()
4.scope=session
fixture为session级别是可以跨.py模块调用的,运行一次程序只会调用一次
也就是当我们有多个.py文件的用例的时候,如果多个用例只需调用一次fixture,那就可以设置为scope="session",并且写到conftest.py文件里作为全局的fixture
conftest.py 点击查看文件名称时固定的,pytest会自动识别该文件。
放到项目的根目录下就可以全局调用了,如果放到某个package下,那就在该package内有效
三、其他参数介绍
1.params
一个可选的参数列表,它将导致被fixture装饰的测试用例以列表中每个列表项为参数,多次调用fixture功能
1.fixture可以带参数,params支持列表;
2.默认是None;
3.对于param里面的每个值,fixture都会去调用执行一次,就像执行for循环一样把params里的值遍历一次。
在 pytest 中有一个内建的 fixture 叫做 request,代表 fixture 的调用状态。request 有一个字段 param,使用类似@pytest.fixture(param=tasks_list)
的方式给fixture传参,在 fixture 中使用 request.param
的方式作为返回值供测试函数调用。其中 tasks_list 包含多少元素,该 fixture 就会被调用几次,分别作用在每个用到的测试函数上
如下fixture和测试用例都执行三遍相当三个测试用例,场景如:测试三组账户密码登录,登陆用例都是一个只是每次数据不同
import pytest tasks_list = [(10,11),(20,22),(33,33)] @pytest.fixture(params=tasks_list) def test_data(request): print(f"fixture得到:账号:{request.param[0]},密码:{request.param[1]}" ) return request.param class TestData: def test_1(self,test_data): print("用例:",test_data) if __name__ == '__main__': pytest.main()
2.ids
ids通常可以与params一起使用,在没有指定 id情况下,在输出时 pytest 会自动生成一个标识作为测试ID:
- 当params列表项是数字、字符串、布尔值和None时,将使用列表项自身字符串形式表示测试ID,如[True1] [True2] [xxoo] [123] [None]等
- 对于其他对象,pytest会根据参数名称创建一个字符串,如params中截图显示的[test_data0] [test_data1]
- 可以通过使用
ids
关键字参数来自定义用于指定测试ID,例如@pytest.fixture(param=tasks_list,ids=task_ids) , ids可以是列表,也可以是函数供 pytest 生成 task 标识。
import pytest data_list = [(10,11),(20,22),(33,33)] @pytest.fixture(params=data_list, ids=["a","b","c"]) def tes_data(request): print(f"fixture得到:账号:{request.param[0]},密码:{request.param[1]}" ) return request.param class TestData: def test_1(self,tes_data): print("用例:",tes_data) if __name__ == '__main__': pytest.main()
3.autouse
默认False不开启
当用例很多的时候,每次都传fixture,会很麻烦。fixture里面有个参数autouse,默认是False没开启的,可以设置为True开启自动使用fixture功能,这样用例就不用每次都去传参了
autouse设置为True,自动调用fixture功能,无需传仍何参数,作用范围跟着scope走(谨慎使用)
import pytest @pytest.fixture(scope='module', autouse=True) def test1(): print('开始执行module') @pytest.fixture(scope='class', autouse=True) def test2(): print('开始执行class') @pytest.fixture(scope='function', autouse=True) def test3(): print('开始执行function') def test_a(): print('---用例a执行---') def test_d(): print('---用例d执行---') class TestCase: def test_b(self): print('---用例b执行---') def test_c(self): print('---用例c执行---')
========================================================================================================================= test session starts ========================================================================================================================== platform win32 -- Python 3.8.0, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- E:\python3.8\python.exe cachedir: .pytest_cache rootdir: D:\代码\自动化测试\pytest_test, configfile: pytest.ini, testpaths: ./dir01/dir01_test.py collected 4 items dir01/dir01_test.py::test_a 开始执行module 开始执行class 开始执行function ---用例a执行--- PASSED dir01/dir01_test.py::test_d 开始执行class 开始执行function ---用例d执行--- PASSED dir01/dir01_test.py::TestCase::test_b 开始执行class 开始执行function ---用例b执行--- PASSED dir01/dir01_test.py::TestCase::test_c 开始执行function ---用例c执行--- PASSED ================================================================================================================================ PASSES ================================================================================================================================
4.name
- fixture的重命名
- 默认为 fixture 装饰的的函数名,但是 pytest 也允许将fixture重命名
- 如果使用了name,只能将name传入,函数名不再生效
四、fixture的teardown后置操作
1.使用yield实现
前几章中都是前置操作,后置操作需要用到python的 yield来实现,yield语法讲解点这里:>>迭代器生成器<<
- 如果yield前面的代码,即setup部分已经抛出异常了,则不会执行yield后面的teardown内容
- 如果测试用例抛出异常,yield后面的teardown内容还是会正常执行
import pytest @pytest.fixture(scope="session") def open(): # 整个session前置操作setup print("===打开浏览器===") test = "测试变量是否返回" yield test # 整个session后置操作teardown print("==关闭浏览器==") @pytest.fixture def login(open): # 方法级别前置操作setup print(f"输入账号,密码先登录{open}") name = "==我是账号==" pwd = "==我是密码==" age = "==我是年龄==" # 返回变量 yield name, pwd, age # 方法级别后置操作teardown print("登录成功") def test_s1(login): print("==用例1==") # 返回的是一个元组 print(login) # 分别赋值给不同变量 name, pwd, age = login print(name, pwd, age) assert "账号" in name assert "密码" in pwd assert "年龄" in age def test_s2(login): print("==用例2==") print(login) if __name__ == '__main__': pytest.main()
2.使用request.addfinalizer终结函数实现
yield是返回数据并暂停,后置操作在yield后面。
终结函数是用return返回,终结函数的后置操作处于前置和return中间。
- 如果request.addfinalizer()前面的代码,即setup部分已经抛出异常了,则不会执行request.addfinalizer()的teardown内容(和yield相似,应该是最近新版本改成一致了)
- 可以声明多个终结函数并调用
import pytest @pytest.fixture(scope="module") def test_addfinalizer(request): # 前置操作setup print("==打开浏览器==") test = "test_addfinalizer" def fin(): # 后置操作teardown print("==关闭浏览器==") request.addfinalizer(fin)
# 返回前置操作的变量 return test def test_anthor(test_addfinalizer): print("==最新用例==", test_addfinalizer) if __name__ == '__main__': pytest.main()