pytest基础004-pytest fixture 用法

pytest中可以使用 @pytest.fixture 装饰器来装饰一个方法,被装饰的方法名可以作为一个参数传入到测试方法中。可以使用这种方式来完成测试之前的初始化,也可以返回数据给测试函数。

fixture的作用

- 定义传入测试中的数据集
- 配置测试前系统的初始状态
- 为批量测试提供数据源

fixture的用法

- 类似 setup,teardown 功能,但比它们更灵活
- 直接通过函数名调用或者使用装饰器 @pytest.mark.usefixtures('login')
- 允许使用多个 fixture
- 使用 autouse 自动应用,如果要返回值,需要传 fixture 函数
- 作用域(session > module > class > function)

应用-场景1:

  • 测试用例执行时,有的测试用例需要登录,有的测试用例无需登录
  • setup 和 teardown 无法满足
调用方式
- 测试用例中传入方法名
- @pytest.mark.usefixtures('login')
- 自动调用:@pytest.fixture(autouse=True)

代码如下:

import pytest

@pytest.fixture()
def login():
    print("登录操作")
    yield
    print("注销操作")

@pytest.fixture()
def login3():
    print("登录操作3")
    yield
    print("注销操作3")


def test_one(login):
    print("用例one,执行前先登录")


def test_two():
    print("用例two,无需登录")

# 可以叠加多个 @pytest.mark.usefixtures() ,先执行的放底层,后执行的放上层
@pytest.mark.usefixtures("login")
@pytest.mark.usefixtures("login3")
def test_three():
    print("用例three,执行前先登录")

# 设置autouse=True 所有测试用例都会执行登录操作
# @pytest.fixture(autouse=True)
def login2():
    print("登录操作2")
#
# def test_four():
#     print("用例4,执行前先登录2")

# 在类声明上面加 @pytest.mark.usefixtures() ,
# 代表这个类里面所有测试用例都会调用该fixture
@pytest.mark.usefixtures("login")
class TestDemo:

    def test_five(self):
        print("用例5 需要登录操作")

    def test_six(self):
        print("用例6 需要登录操作")

应用-场景2:

  • 整个模块有多条测试用例,执行全部用例前都需要打开浏览器;全部执行后需要关闭浏览器
  • 打开和关闭浏览器的操作只需要执行一次(每次都执行打开操作,非常占用系统资源)
  • 这种场景除了 setup_module/teardown_module 实现,还可以设置模块级别的 fixture 装饰器来实现。
作用域
- 控制方法:@pytest.fixture(scope="")
- scope的取值:
    - function(默认) 函数或者方法级别都会被调用
    - class  类级别的调用
    - module  模块级别调用一次
    - session  多个文件/包 调用一次(可以跨.py 文件调用,每个.py 文件就是module)

代码如下:

import pytest

@pytest.fixture(scope='module')
def open():
    print("打开浏览器")
    yield
    print("关闭浏览器")

class TestSearch:

    def test_search1(self,open):
        print("test_search1")
        # 要想引发异常,最简单的形式就是输入关键字raise,后跟要引发的异常的名称。
        # 关键字raise是用来抛出异常的,一旦抛出异常后,后续的代码将无法运行。
        raise NameError

class TestSearch2:

    def test_search2(self,open):
        print("test_search2")

@pytest.mark.usefixtures('open')
def test_search():
    print("test_search3")

运行结果:

test_feature_scope.py::TestSearch::test_search1 打开浏览器
test_search1
FAILED
test_feature_scope.py::TestSearch2::test_search2 test_search2
PASSED
test_feature_scope.py::test_search test_search3
PASSED关闭浏览器

fixture 方法返回值的获取
- 在测试用例中使用 fixture 方法名 就可以获取到 yield 后面的返回值
- 具体代码见下面的代码
import pytest

@pytest.fixture(scope="class")
def login():
    print("登录方法")
    username = 'admin'
    password = '123456'
    yield [username,password]
    print("退出登录")

class TestLogin:

    def test_login(self,login):
        print(f"用户的信息是:{login}")
        print("这是一条 test_login 用例")

    def test_login2(self,login):
        print("这是一条 test_login2 用例")

运行结果:

test_login2.py::TestLogin::test_login 登录方法
用户的信息是:['admin', '123456']
这是一条 test_login 用例
PASSED
test_login2.py::TestLogin::test_login2 这是一条 test_login2 用例
PASSED退出登录

应用-场景3

  • 多个测试开发工程师一起开发时,用到相同的功能
  • 把公共模块放到方便大家调用的文件中,conftest.py 文件中
conftest.py 文件 使用规则
- conftest.py 这个文件名是固定的,不可以更改
- conftest.py 与运行用例在同一个包下,并且该包中有 __init__.py 文件
- 使用的时候不需要导入 conftest.py 文件, pytest 会自动识别到这个文件
- 所有同目录测试文件运行前都会执行 conftest.py 文件
- 放在项目的根目录下可以全局调用,放在 package 下,就在这个 package 下生效
- 全局的配置和前期的工作都可以写在这里

案例目录结构如下:

创建conftest.py 定义了公共方法,代码如下:

import pytest

@pytest.fixture(scope='session')
def connectDB():
    print("sub demo下的 connectDB 连接")
    yield
    print("关闭数据库")

创建 test_demo2.py 文件,代码如下:

import pytest

# @pytest.fixture()
# def connectDB():
#     print("test_demo2 下的 connectDB ")

def test_a(connectDB):
    print("sub demo 下的 test_a")

class TestA():

    def test_b(self):
        print("sub demo 下的 test_b")

创建check_demo.py 文件,代码如下:

def test_demo(connectDB):
    print("check demo 下的 test_demo方法")

def check_demo():
    print("check demo 下的 check_demo方法")

class CheckDemo:

    def check_demo2(self):
        print("CheckDemo check_demo2")

执行结果如下:

test_demo2.py::test_a sub demo下的 connectDB 连接
sub demo 下的 test_a
PASSED
test_demo2.py::TestA::test_b sub demo 下的 test_b
PASSED
check_demo.py::test_demo check demo 下的 test_demo方法
PASSED
check_demo.py::check_demo check demo 下的 check_demo方法
PASSED
check_demo.py::CheckDemo::check_demo2 CheckDemo check_demo2
PASSED关闭数据库

fixture 传递参数

  • 测试过程中需要大量的测试数据,如果每条测试数据都编写一条测试用例,用例数据是非常庞大的。
  • 一般我们在测试过程中会将测试用例用到的数据以参数的形式传入到测试用例中,并为每条测试数据生成一个测试结果。
  • 方法: @pytest.fixture(params=[1,2,3]),传入的数据是列表,传入的数据使用固定的参数名 request 来接收。

代码如下:

import pytest
@pytest.fixture(params=[1,2,3])
def login1(request):
    data= request.param
    print("获取数据")
    return data

def test_case(login1):
    print(login1)
    print("测试用例1")

运行结果:

test_params.py::test_case[1] 获取数据
1
测试用例1
PASSED
test_params.py::test_case[2] 获取数据
2
测试用例1
PASSED
test_params.py::test_case[3] 获取数据
3
测试用例1
PASSED

pytest 配置

(如何避免warnings)

  • 写在 pytest.ini 文件中
  • 放在项目工程的根目录
  • 不能用任何中文符号
[pytest]
markers  自定义 mark 标签名
addopts   运行时参数(可添加多个命令行参数,空格分隔,所有参数与命令行一致)
python_files  自定义测试文件命名规则
python_classes = Test_*   自定义测试类命名规则
python_functions= test_* check_*    自定义测试方法命名规则
testpaths = bilibili baidu    指定特定路径运行
norecursedirs = result logs datas test_demo*   运行时忽略某些文件夹

创建pytest.ini 文件,代码如下:

[pytest]
markers = bookstore
          login
          add
          sub
addopts = -vs

;自定义测试文件命名规则
python_files = test_* check_*
;自定义测试类
python_classes = Test* Check*
;自定义测试方法
python_functions = test_* check_*
;指定特定路径:当在上级路径时,也会默认执行sub_demo
;testpaths = sub_demo
;忽略路径
norecursedirs = result logs datas
  • 如果不想标记login的用例,我们直接取反即可
    pytest -s -m "not login" login.py
  • 如果想执行多个自定义标记的用例
    pytest -s -m "login or add" test_demo.py

关于fixture的注意点

添加了 @pytest.fixture ,如果fixture还想依赖其他fixture,需要用函数传参的方式,不能用 @pytest.mark.usefixtures() 的方式,否则会不生效

@pytest.fixture(scope="session")
def open():
    print("===打开浏览器===")

@pytest.fixture
# @pytest.mark.usefixtures("open") 不可取!!!不生效!!!
def login(open):
    # 方法级别前置操作setup
    print(f"输入账号,密码先登录{open}")

fixture之yield实现teardown

用fixture实现teardown并不是一个独立的函数,而是用 yield 关键字来开启teardown操作

import pytest


@pytest.fixture(scope="session")
def open():
    # 会话前置操作setup
    print("===打开浏览器===")
    test = "测试变量是否返回"
    yield test
    # 会话后置操作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)

yield注意事项

  • 如果yield前面的代码,即setup部分已经抛出异常了,则不会执行yield后面的teardown内容
  • 如果测试用例抛出异常,yield后面的teardown内容还是会正常执行

yield+with的结合

# 官方例子
@pytest.fixture(scope="module")
def smtp_connection():
    with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
        yield smtp_connection  # provide the fixture value

该 smtp_connection 连接将测试完成执行后已经关闭,因为 smtp_connection 对象自动关闭时, with 语句结束。

addfinalizer 终结函数

@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)

注意事项
  • 如果 request.addfinalizer() 前面的代码,即setup部分已经抛出异常了,则不会执行
  • request.addfinalizer() 的teardown内容(和yield相似,应该是最近新版本改成一致了)
  • 可以声明多个终结函数并调用

写在最后的话:小白同学愿意和大家一起成长~

posted on 2021-07-03 18:50  yao_murcy  阅读(327)  评论(0编辑  收藏  举报