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()进行叠加,先执行的放底层,后执行的放上层

View Code

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()
View Code

可以先画出继承关系图,这样就会很明了,按着等级去找就对了:

在实例化基本规则大前提下,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()
View Code

 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()
View Code

 

 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()
View Code

 

 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执行---')
自动使用fixture
========================================================================================================================= 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()

参考:

辉辉辉辉a

小菠萝

全栈测试开发日记

 

posted @ 2022-02-21 12:44  www.pu  Views(2371)  Comments(0Edit  收藏  举报