三、Pytest框架

Pytest是什么?

  • pytest能够支持简单的单元测试和复杂的功能测试;
  • pytest可以结合Requests实现接口测试;
  • 结合Selenium、Appium实现自动化功能测试;
  • 使用pytest结合Allure集成到Jenkins中可以实现持续集成。
  • pytest支持315种以上的插件;

1、安装pytest及其插件

pytest生态是由pytest本身和pytest插件共同构成的

  • pytest:框架本体
  • pytest-html:生成HTML测试报告
  • pytest-xdist:并行话执行测试用例
  • pytest-rerunfailures:失败重跑
  • pytest-ordering:为用例排序
  • allure-pytest:生成allure测试报告

1.安装pytest

pip是Python包管理工具,使用pip工具进行安装

pip install pytest 

2.安装pytest插件

pip install pytest pytest-html pytest-xdist pytest-rerunfailures pytest-ordering allure-pytest 

2、Pytest规范

1.pytest用例规范

1. 文件:必须以test_ 开头或_test结尾
2. 类:以Test开头或者继承unittest.TestCase的任意名称的类
3. 方法:以test_开头的方法
4. 注意:测试类中不可添加__init()__构造函数,pytest不识别含__init()__的类内的测试方法

image-20231017150111306

2.pytest输出规范

image-20231017150121912

Text测试报告-》HTML测试报告

1. 报告头
   1. 平台和版本信息
   2. 根目录、配置文件、特殊选项
   3. 插件列表
2. 收集情况
   1. 测试用例的数量
3. 执行状态
   1. 用例的执行结果
   2. 测试执行进度

3、python运行pytest

  • pthon解释器

    • 使用python test_*文件名.py 执行方法当中的pytest.main()
    • 使用python -m pytest test_*文件名.py方式执行pytest.main()
  • 使用pytest方法执行p

对于一个简单的测试用例

def inc(x):
  return x + 1

def test_anwer():
  assert inc(4)

1.使用pytest执行

pytest test_sample.py

image-20231017150452054

2.使用python解释器执行

方法1:python test_*文件名.py
方法2:python -m pytest test_*文件名.py

在使用这两个命令之前,我们先来熟悉一下pytest.main()函数及其模块

使用python解释器执行文件,系统会以main()函数作为入口执行代码程序,调用pytest.main()执行所有的满足pytest规则的方法。

为了确保测试用例只在本模块调用执行,我们需将pytest.main()方法放在if __name__ == '__main__':模块内

pytest.main()也可以传入命令行参数达到在终端使用pytest命令行一样的效果,如下:

if __name__ == '__main__':  # 只在本模块运行
  pytest.main()  # 1、直接调用,运行当前目录下所有满足pytest条件的用例
  pytest.main(['test_command_param.py::test_function', '-vs'])  # 2、运行子模块下的方法,并使用标签,每个值需要用引号隔开 相当于 pytest test_command_param.py::test_function -vs
  pytest.main(['test_command_param.py', '-vs', '-m', 'double'])  # 3、运行某个标签下的所有用例 相当于 pytest test_command_param.py -vs -m=double
2.1 使用python test_*文件名.py

首先,在代码中添加一个模块,将命令和标签以列表的形式传到pytest.main(参数)内

if __name__ == '__main__':  # 只在本模块运行
  pytest.main()  # 1、直接调用,运行当前目录下所有满足pytest条件的用例
  pytest.main(['test_command_param.py::test_function', '-vs'])  # 2、运行子模块下的方法,并使用标签,每个值需要用引号隔开
  pytest.main(['test_command_param.py', '-vs', '-m', 'double'])  # 3、运行某个标签下的所有用例

调用python解释器

python +文件名.py

image-20231017162653356

2.2 使用python -m pytest test_*文件名.py

我们还可以使用以下命令去实现,效果等价于python + test_*.py文件名方式

python -m pytest test_*.py

image-20231017162729558

综上所述,实际执行用例环境是在python环境下,因此常用python解释器去执行测试用例,及python -m pytest test或python +文件名.py的方式,因为而不常用pytest框架的方式,使用python解释器方便指定版本。

3.pytest运行多条用例

image-20231017151200003

  • 执行包下面所有的用例:pytest
  • 执行单独一个pytest模块:pytest 文件名.py

image-20231017151213129

  • 运行某个模块里面的某个类:pytest 文件名.py::类名 (-v显示细节)

image-20231017151253495

  • 运行某个模块里面某个类的方法:pytest 文件名.py::类名::方法名(模块其实就是一个文件里面有很多类)

image-20231017151318005

pytest常见提示关键字:

PASSED :表示断言通过

FAILED:表示断言失败

ERROR:表示代码命名等不符合规范,且不能运行

WARNING :警告,不影响正常运行

4、setup和teardown

image-20231017151339816

setup/teardown与setup_method/teardown_method功能相同,相当于一个简单的缩写。

这里注意函数与方法的区别:函数(function)相对于方法(method)而言函数定义在类外,方法定义在类里面作为类独有的方法。这里setup_function和setup_method就是属于在函数或方法范围上的不同。

def test_case1():
  print("case1")


def setup_function():
  print("资源准备:setUp function...")


def teardown_function():
  print("资源销毁:tearDown function...")

运行结果:

image-20231017151402844

及最高级是setup_module与teardown_module(模块级)整个程序只前后运行一次,最低级别的是setup、teardown(方法级)每个方法前后调用。

def test_case1():
  print("case1.")


def test_case2():
  print("case2.")


def setup_module():
  print("资源准备:setUp module...")


def teardown_module():
  print("资源准备:teardown module...")


def setup_function():
  print("资源准备:setUp function..")


def teardown_function():
  print("资源销毁:tearDown function..")

运行结果:

image-20231017151424908

5、pytest基本命令行参数

更多筛选用例参数

Pytest脚本的运行_pytest怎么执行指定文件或目录-CSDN博客

image-20231017151442319

--help            查看帮助
-x                用例一旦失败(fail/error),就立即停止执行。一般用在冒烟测试(单独测试几个核心的功能的测试,一旦发现错误,版本立即打回给开发)          
--maxfail=mun    允许失败的个数=mun,当失败个数超过num停止执行。相比较-x没有那么苛刻
-m                标记用例
-k                执行包含某个关键字的测试用例
-v                显示用例执行详细信息
-s                打印输出日志(-vs一块使用)
-collection-only  测试平台,pytest自动导入功能
-h 显示所有参数帮助 
-n X 使用X个进程,并行化执行用例(1核执行时间为10s,两核执行时间缩短一半5s)/-n auto 自动选择进程数执行用例(取决于你的电脑cpu)
--html=Path 生成HTML测试报告和一个样式文件,并保存在Path路径下/--html=Path --self-contained-html HTML文件自包含样式文件,只会生成一个HTML文件
reruns X 测试用例失败后,重新X次

1.-x 用例失败停止执行

用例一旦执行错误,立刻停止执行

错误用例在最后一个:

image-20231017151615669

image-20231017151620040

错误用例在第一个:

image-20231017151627599

image-20231017151632379

2.–maxfail=mun 设置允许失败的用例个数num

当我们使用–maxfail=mun命令设置最大允许失败的用例个数为2,我们把失败的用例放在第一条时,发现不再和-x一样直接退出,而是完整的执行完整个测试类,直到找到两个失败的用例才退出。

image-20231017151647310

image-20231017151651225

而我们将失败次数改为1时,本质上和-x没区别,当第一条用例失败时直接退出

image-20231017151702710

3.-m 标记用例

用例用mark分组后,使用-m标签选择指定组执行用例

-m=double
-m double
-m "double"
-m 'double'

image-20231017151723345

4.-k 执行方法名包含某个关键字的用例

pytest test_command_param.py -v --maxfail=3 -k "double"

image-20231017151742540

image-20231017151747093

执行一个测试文件内的两个方法

pytest -k "test_method1 or test_method2"
# 这个命令将运行 test_method1 和 test_method2,无论它们位于哪个文件中。

如果你的测试方法分布在不同的类中,你也可以在 -k 表达式中包含类名。例如:

pytest -k "TestClass1 and test_method1 or TestClass2 and test_method2"
# 这将运行 TestClass1 中的 test_method1 和 TestClass2 中的 test_method2

5.-v 输出测试详情

如果不使用-v,则用例成功用"."标识,失败用"F"表示,不方便阅读

image-20231017151800262

使用-v,会显示详细的用例方法是否通过

image-20231017151808993

6.-s 开启内容输出

pytest框架采用终端的方法默认关闭测试用例的输出内容,使用-s会打开并输出内容到控制台,一般与-v一起使用用于调试代码

pytest test_command_param.py -vs

image-20231017151834276

image-20231017151837983

7.–collected-only 只收集用例

image-20231017151851880

8.–help显示帮助

pytest --help

image-20231017151908490

我们还可以使用过滤符grep来选择目的标签查找帮助:

pytest --help|findstr 标签 # windows的过滤关键字为findstr
pytest --help|grep 标签 # linux和mac系统

使用help来查找mark帮助文档:

image-20231017151927757

9.–lf(–last-failed) 只执行上次失败的用例

image-20231017151939820

我们先执行一条失败的用例

image-20231017151947954

然后使用命令–lf -v,发现只执行了上次失败的那条用例test_double3

image-20231017151956707

10.–ff(–failed-first) 先执行上次失败的用例,再执行剩余的用例

image-20231017152006268

11.-n X 使用X个进程,并行化执行用例

(1核执行时间为10s,两核执行时间缩短一半5s),-n auto 自动选择进程数执行用例(取决于你的电脑cpu,四核n=4 8核n=8)

pytest多进程/多线程执行测试用例 - 网名余先生 - 博客园 (cnblogs.com)

image-20231017152038446

12.–html=Path 生成HTML测试报告和一个样式文件

–html=Path 生成HTML测试报告和一个样式文件,并保存在Path路径下

image-20231017152147810

–html=Path --self-contained-html HTML文件自包含样式文件,只会生成一个HTML文件

image-20231017152154903

6、pytest.ini配置化文件

在执行测试用例时会先执行pytest.ini配置文件,我们可以把所有的命令都统一放在pytest.ini文件进行配置,当执行命令pytest时候,会先自动加载pytest.ini文件并执行里面的配置

[pytest] 
addopts = -v -s -n 1 --html=report.html --self-contained-html 

下次执行直接使用pytest命令,系统自动加载pytest.ini配置文件里的命令,无需在命令行中额外加命令

7、Mark标签

image-20231017152305625

1.Mark.标签名 设置标签组

首先我们要再测试用例上使用解释器定义mark标签

image-20231017152317492

然后,再使用-m标签进行标签选择执行,有三种选择方式都可

-m=double
-m double
-m "double"
-m 'double'

image-20231017152337581

另外,以上都是我们自定义的一个标记,pytest识别不出会给我们抛出警告,并不影响运行

image-20231017152344921

我们可以在pytest.ini配置文件中配置标签名,这样pytest就能自动识别出我们的标签不会给警告了

在pytest.ini配置标签名

image-20231017152357788

再执行用例发现不再报警告

image-20231017152414446

2.自定义mark

mark首先在pytest.ini配置文件里面定义,才可以使用

定义mark配置信息:将mark自定义分为三类:test1、test2、test3,

即markers = 类型名 : 类型注释

image-20231017152433978

你可以用mark标记不同的测试用例,通过 pytest -m 类型名 筛选需要执行哪类测试用例

  • 选择这类测试用例: pytest -m 类型名
  • 不选择这类测试用例: pytest “-m not” 类型名 由于not会当成类型名,因此我们要用” “包裹

image-20231017152505665

执行标签用例

image-20231017152515087

3.Mark.skip 跳过测试用例

image-20231017152626079

使用场景

image-20231017152632970

方法1:添加装饰器:
  @pytest.mark.skip 为方法级别的跳过,也可以用作类级别的跳过
  @pytest.mark.skipif 为方法级别的跳过,也可以用作类级别的跳过
方法2:代码中添加跳过后面的代码
  pytest.skip(reason)
3.1 skip跳过
@pytest.mark.skip(reason="代码没有实现") # 在用例前面添加skip装饰器,reason上输出跳过提示
def test_zero():
  assert 0 == func(0)

image-20231017152713637

3.2 skipif代码跳过
# skipif的用法
@pytest.mark.skipif(条件表达式, reason="")
# 当条件表达式返回True时,会跳过用例

@pytest.mark.skipif(1 == 1,reason='满足条件跳过此用例') # 跳过用例并输出reason信息
def test_pytest_1():
  print('case4')

@pytest.mark.skipif(1 == 2,reason='不满足条件不会跳过此用例')
def test_pytest_2():
  print('case5')

实例,当平台操作系统为win,且python版本高于3.6时跳过用例

print(sys.platform)  # 输出操作系统平台 win为windows,darwin为mac


# 判断如果为win操作系统,跳过该用例并输出信息
@pytest.mark.skipif(sys.platform == 'win32', reason="does not run windows")
def test_case1():
  assert True


# 判断如果为mac操作系统,跳过该用例并输出信息
@pytest.mark.skipif(sys.platform == 'darwin', reason="does not run mac")
def test_case2():
  assert True


# 判断python版本信息小于3.6,跳过用例给出信息
@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or lower")
def test_case3():
  assert True

image-20231017152748253

3.3 skip代码内判断跳过

创建一个检查登录方法,和一个测试方法

def check_login():  # 检查登录方法
  return False


def test_function():  # 测试方法
  print("start...")

  if not check_login():  # 如果登录不成功,跳过后续步骤,并返回reason信息
      pytest.skip("登录未成功...") # 用代码实现步骤跳过

  print("end...")

执行测试用例:当if判断错误时调用pytest.skip跳过后续代码,并输出跳过reason

image-20231017152816084

4.Mark.xfail 标签

期望测试用例为失败并标记,失败不会影响其他测试用例执行

image-20231017152832940

xfail相比较于skip来说,虽然使用的格式类似,但是xfail不会跳过用例而是执行测试用例,成功输出xpass,失败输出xfail

xfail相比于skip,reason任何时候都会输出

# 用法1:加上装饰器@pytest.mark.xfail
@pytest.mark.xfail(reason="bug case1") 
def test_case1():
  print("test_xfail1方法执行...")
  assert 1 == 2

# 我们也可以定义xfail变量为pytest.mark.xfail,下次调用直接使用@xfail便捷调用
xfail = pytest.mark.xfail

@xfail(reason="bug case2")
def test_case2():
  print("test_xfail方法执行...")
  assert 1 == 1

image-20231017152846960

xfail内部代码跳过,与skip跳过不同的是,xfail没有判断条件,执行完xfail无论如何都会跳过后面的代码

def test_xfail():
  print("*****开始测试*****")
  pytest.xfail(reason="该功能尚未完成") # xfail在代码内会暂停继续执行下面的代码
  print("测试过程")
  assert 1 == 1

image-20231017152905636

5.Mark标签的参数传递

@pytest.mark.parametrize标签具有给测试用例传递参数的作用,如:

@pytest.mark.parametrize('username,password,result', [('qwx13057573527', 'qwx#125617', 'qwx13057573527'),('qwx', 'qwx#125617', '用户名错误'),('qwx13057573527', 'qwx', '密码错误')],ids=('test_login_001', 'test_login_002', 'test_login_003'))
def test_login(username, password, result, open_page):

mark.parametrize标签传递的参数如username,password,result都会一一传入测试用例的参数username,password,result当中。ids表示测试用例的名称。

当你使用 pytest.mark.parametrize 并传入多个参数时,pytest 会自动遍历并匹配它们的元素。如果你传递的 login_success_datalogin_ids 的长度相等且元素顺序一一对应,pytest 会在运行测试时,将它们按对应的顺序进行匹配。且每组测试数据都会跑一次用例。

将以上的案例的测试数据封装在类当中,如下所示

class LoginData(object):
    """用户登录测试数据"""

    login_success_data = [("qwx13057573527", "qwx#125617", "qwx13057573527")]

封装之后再调用测试数据如下

class TestLogin(object):
    """登录用例"""

    login_data = LoginData()

    @pytest.mark.parametrize(
        "username, password, expect", login_data.login_success_data,ids=LoginData.login_ids
    )
    def test_login_success(self, ini_page, username, password, expect):
        login_page = ini_page
        login_page.login(username, password)
        account = login_page.get_account_text()
        assert expect in account

7、Fixture

fixture是pytest中特殊的一部分,也是必须掌握的核心用法

fixture的作用是: 使测试用例执行更加可靠,结果也更加稳定 可以称之为软件测试的装置、夹具、脚手架,作用和目的: 将多个用例里面的重复部分提取到fixture方法内 ,因此很适合用来做自动化准备测试环境,参数化多个测试用例的数据

作用:

1. 在测试用例执行之前,自动化准备相关的测试环境

2. 在测试执行之后,将相关内容进行销毁

1.fixture案例

案例:断言"百度",“阿里”,"腾讯"网站标题是否包含关键字

image-20231017152939311

访问baidu.com,标题中应该有"百度"

    1. 打开浏览器
    2. 输入https://baidu.com
    3. 获取标题
    4. 断言"百度"出现在标题当中
""" -打开浏览器 -输入https://baidu.com -获取标题 -断言“百度”出现在标题中 :return: """


def test_baidu():
  # 自动安装webdriver
  _path = ChromeDriverManager(url="https://npm.taobao.org/mirrors/chromedriver").install()
  # 启动浏览器
  driver = webdriver.Chrome(service=Service(_path))
  # 输入网址
  driver.get("https://baidu.com")
  # 获取标题
  title = driver.title
  # 断言标题
  assert "百度" in title


def test_aliyun():
  # 自动安装webdriver
  _path = ChromeDriverManager(url="https://npm.taobao.org/mirrors/chromedriver").install()
  # 启动浏览器
  driver = webdriver.Chrome(service=Service(_path))
  # 输入网址
  driver.get("https://aliyun.com")
  # 获取标题
  title = driver.title
  # 断言标题
  assert "阿里" in title


def test_qq():
  # 自动安装webdriver
  _path = ChromeDriverManager(url="https://npm.taobao.org/mirrors/chromedriver").install()
  # 启动浏览器
  driver = webdriver.Chrome(service=Service(_path))
  # 输入网址
  driver.get("https://qq.com")  # 获取标题
  title = driver.title  # 断言标题
  assert "腾讯" in title

由于我们写的案例目的是断言网站标题含有目的文字,而打开浏览器等操作不属于我们设计用例的一部分,属于测试用例的准备工作。

  • 对于 测试用例的准备工作 ,挪出测试用例代码
  • 对于重复性的代码, 使用函数进行重用
  • 这样的工作应该交给 fixture 完成

我们可以使用装饰器 @pytest.fixture 创建一个fixture方法

import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

@pytest.fixture  # 定义一个fixture夹具名driver 
def driver():
  _path = ChromeDriverManager().install()  # 自动安装webdriver 
  return webdriver.Chrome(service=Service(_path))  # 启动浏览器 

def test_baidu(driver):  # 将fixture夹具名传入测试用例 
  driver.get("https://baidu.com")
  title = driver.title
  assert "百度" in title

def test_aliyun(driver):  # 将fixture方法传入函数 
  driver.get("https://aliyun.com")
  title = driver.title
  assert "阿里" in title

def test_qq(driver):  # 将fixture方法传入函数 
  driver.get("https://qq.com")
  title = driver.title
  assert "腾讯" in title

以上的网站都是打开之后关闭再打开,所有的用例之间fixture不能共用,我们可以创建一个fixture完成浏览器启动的共用。

2.fixture范围

我们可以在创建fixture夹具装饰器的时候添加一个范围

@pytest.fixture(scope='session') # fixture范围为一个会话 

fixture范围:

1. function:同一个函数中的测试用例**(最小/默认值)**

2. class:同一个类中的测试用例

3. module:同一个模块(文件)中的测试用例

4. package:同一个包(文件夹)中的测试用例

5. session:整个测试活动**(最大)**

3.fixture参数 request

fixture标签参数autouse=True 表示在该fixture影响范围内每个测试函数都无需显示调用便可使用这个fixture,如实现失败截图的fixture。

@pytest.fixture(scope="function", autouse=True)
def screenshot_on_failure(request, driver):
    """在测试失败时自动截图"""
    yield  # 继续执行测试
    if request.node.rep_call.failed:  # 检查测试是否失败
        timestamp = time.strftime("%Y%m%d_%H%M%S")
        test_name = request.node.name
        screenshot_path = f"screenshots/{test_name}_{timestamp}.png"
        driver.save_screenshot(screenshot_path)
        print(f"截图已保存: {screenshot_path}")

scope="function"autouse=Truepytest 中 fixture 的两个不同参数,它们分别用于不同的目的,并不会冲突。scope="function", autouse=True表示在每个测试用例的函数级别都会显示调用screenshot_on_failure方法

request参数是pytest当中的一个特殊对象,提供对测试上下文和测试执行信息的访问,如通过 request 对象,你可以获取测试函数的元数据、访问当前的测试用例名称、控制测试执行过程、动态添加清理操作等。request.node.rep_call.failed用于获取测试是否失败

autouse=True 允许 pytest 在每个测试执行时自动调用该 fixture,但它只会自动执行和应用其中的副作用(如执行一些初始化或清理工作),而不会将返回值自动注入到测试用例中。

如果 fixture 有返回值,并且你想在测试用例中使用这个返回值(比如 ValidationHandler 对象),你仍然需要通过测试函数的参数来显式传递它。

@pytest.fixture(scope="class", autouse=True)
def validator():
    # 该 fixture 将返回 ValidationHandler 对象
    return ValidationHandler()
def test_example_case(validator: ValidationHandler):
    # 显式传入 validator
    response = requests.Response()  # 假设这是某个请求返回的响应
    validation_data = {"access_token": "some_value"}

    # 使用 validator 的方法
    validator.validate_response(response, validation_data)

访问测试函数名称: 你可以通过 request.node.name 获取当前测试函数的名称,在日志记录或截图文件命名时使用。

def screenshot_on_failure(request, driver):
    test_name = request.node.name  # 获取当前测试函数的名称
    print(f"当前测试用例: {test_name}")

访问测试结果request.node 对象中包含了 rep_call,它提供了测试的执行结果,尤其是在需要判断测试是否失败时很有用。

  • request.node.rep_call.outcome:表示测试的执行结果('passed''failed''skipped')。
  • request.node.rep_call.failed:一个布尔值,表示测试是否失败。
def screenshot_on_failure(request, driver):
    yield
    if request.node.rep_call.failed:
        print("测试失败,将截图。")
        # 截图逻辑...

访问 fixture 参数: 如果你有带参数的 fixture,request.param 可以用来访问传入的参数值。

@pytest.fixture(params=["chrome", "firefox"])
def driver(request):
    if request.param == "chrome":
        driver = webdriver.Chrome()
    elif request.param == "firefox":
        driver = webdriver.Firefox()
    return driver

request 对象的属性

  • request.node: 代表当前测试函数的节点,提供了对测试函数的名称、所属模块、类等信息的访问。

  • request.cls: 如果测试在类中定义,可以通过 request.cls 访问当前测试类。

  • request.function: 访问当前正在执行的测试函数。

  • request.module: 访问当前测试所在的模块。

  • request.session: 访问测试会话对象,包含整个测试运行的信息。

    3.内置fixture

    image-20231017153048132

    8、conftest.py和pytest.ini

1.conftest.py

configtest.py文件是定义在pytest全局下的python文件,用于定义全局fixture和一些钩子函数,常量,配置测试环境等

  • pytest 会在 fixture 被第一次调用时才初始化,fixture可以自定义在fixture文件夹内,如scope=“session”表示全局fixture会在执行测试用例前调用,一般定义在configtest.py内(直接将fixture定义在configtest.py内而不需要显示的调用)

    @pytest.fixture(scope="class")
    def login(logger):
        """初始化Chrome浏览器驱动"""
    
        chrome_options = webdriver.ChromeOptions()
        chrome_options.add_argument("--incognito")  # 启用隐私模式
        driver = webdriver.Chrome(options=chrome_options)
        # 登录网易163浏览器
        driver.get(ENV.url)
        logger.info("启用隐私模式打开浏览器")
        driver.maximize_window()
        logger.info("最大化窗口")
    
        driver.implicitly_wait(20)
        logger.info("设置隐式等待时间为20s")
        yield driver
        driver.quit()
        logger.info("关闭浏览器")
    
  • 可以在 conftest.py 中定义全局的 setupteardown 操作

    @pytest.fixture(scope="session", autouse=True)
    def setup_teardown():
        print("Setup before tests")
        yield
        print("Teardown after tests")
    
  • 可以用于定义全局常量,如文件路径、环境变量等,供测试用例和其他函数使用。

  • pytest通过了一些钩子函数,通常定义在configtest.py内,常见的有自定义参数函数pytest_addoption,初始化钩子函数pytest_configurepytest_configure 是在 整个测试会话开始之前 调用的,这意味着它比任何 fixture 的执行顺序要早)

    # 用于添加自定义命令行选项,允许用户在运行测试时指定额外的参数
    def pytest_addoption(parser):  # config参数由pytest自动传入
        parser.addoption("--env", action="store", default="dev", help="Specify environment: dev, test, or prod")
    
    def pytest_configure(config): # config参数由pytest自动传入
        # 在 pytest 开始执行测试之前,调用自定义的初始化函数
        custom_initialization()
    

2.pytest.ini

pytest.ini 是配置 pytest 的重要工具,通过它可以全局设置测试的行为、日志管理、路径和标记等。它帮助开发者减少在命令行中重复输入参数,提高测试的自动化和管理效率。

  1. 简化命令行参数:通过 addopts 设置默认命令行选项,避免每次执行 pytest 时重复输入相同参数。

    [pytest]
    addopts = -vs --alluredir=report/TestReport/
    
  2. 指定测试路径:使用 testpaths 指定默认的测试文件目录,方便测试文件的管理。

    [pytest]
    testpaths = tests
    
  3. 定义标记:通过 markers 定义自定义标记,便于分类和过滤测试用例。

    [pytest]
    markers =
        smoke: 标记烟雾测试
        regression: 标记回归测试
    
  4. 控制日志输出:可以设置日志的输出级别和输出方式,如将日志输出到文件或控制台。

  5. 过滤警告:使用 filterwarnings 配置忽略特定的警告信息。

  6. 设置文件和类的命名规则:通过 python_filespython_classespython_functions 指定哪些文件、类和函数被视为测试用例。

9、参数化和数据驱动

在实现参数化之前我们先来了解一下什么是数据驱动

数据驱动就是数据的改变从而驱动自动化测试的执行,最终引起测试结果的改变。

简单来说,就是参数化的应用,数据量小的测试用例可以使用代码的参数化来实现数据驱动,数据量大的情况可以使用结构化文件(cvs、yaml、json等)对数据进行存储和在测试用例中进行读取。

CSV(逗号分隔值)文件是一种简单的文本格式,用于存储表格数据,如数字和文本。CSV文件由纯文本组成,每行一个数据记录。每个记录由字段组成,字段之间通常由逗号分隔。这种格式通常用于表示表格数据(比如数据库、电子表格)。

一个典型的CSV文件可能看起来是这样的:

Copy codeColumn1,Column2,Column3
Data1,Data2,Data3
Data4,Data5,Data6

在这个示例中:

  • 第一行是标题行,包含每列的名称。
  • 随后的每一行代表一条记录。
  • 每一行中的每个值(字段)用逗号分隔。

CSV文件的关键特性包括:

  • 简单性:它们是纯文本文件,可以由任何文本编辑器查看和编辑。
  • 通用性:几乎所有的表格处理软件都能识别和处理CSV文件,例如Microsoft Excel、Google Sheets和各种编程语言的库。
  • 灵活性:尽管“CSV”代表逗号分隔值,但实际上可以使用其他字符作为分隔符,如制表符(生成所谓的TSV文件)或分号。

根据您的应用程序和地区设置,CSV文件的确切格式可能略有不同。例如,在一些欧洲地区,分号(;)而不是逗号(,)用作字段分隔符,因为逗号已用作小数点。

CSV文件格式

优点:

1. **简单性和可读性**: CSV文件结构简单,易于理解和编辑,即使是非技术人员也能轻松使用。
2. **广泛支持**: 几乎所有编程语言和数据处理工具都支持CSV格式,无需特殊的解析库。
3. **高效处理大量数据**: 对于大量的行式数据,CSV可以高效处理。

缺点:

1. **结构限制**: CSV不适合复杂的数据结构。它难以表示层次化或嵌套的数据。
2. **缺乏类型信息**: CSV中的所有数据都是文本格式,需要在解析时进行类型转换。
3. **标准不一致**: CSV的格式(如分隔符)可能因地区和用户而异,这可能导致解析问题。
JSON文件格式

优点:

1. **灵活的数据结构**: JSON能够轻松表示嵌套或层次化的数据结构,适合复杂的数据需求。
2. **数据类型识别**: JSON支持基本的数据类型(如数字、字符串、布尔值),因此数据类型在解析时保持不变。
3. **易于解析**: 许多编程语言提供了原生的JSON解析支持,使得读取和写入JSON数据变得简单。

缺点:

1. **文件大小**: 对于同样的数据,JSON文件通常比CSV文件大,因为它包含更多的格式化文本(如键名和括号)。
2. **解析成本**: 解析JSON通常比解析CSV更耗费资源,尤其是在大文件的情况下。
3. **可读性**: 对于非技术人员来说,复杂的JSON结构可能不如CSV那样直观易读。

而参数化的设计方法就是将模型中的定量信息变量化,使之成为任意调整的参数。对于变量化参数赋予不同的数值,就可以得到不同的模型。

参数化可以分为mark标签参数化和fixture参数化

1.mark参数化

mark参数化一般是使用mark.parametrize()进行传参

  • 单参数
  • 多参数
  • 用例重命名
  • 笛卡尔积
@pytest.mark.parametrize('参数1,参数2...', [列表1/元组1,列表2/元组2...]) # 参数与数值要一一对应,如果需要传递多个参数,要放在列表当中嵌套列表/元组
def func(参数1,参数2...) # 形参与参数化的参数要一一对应
1.1 单参数化

由于search_list是一个列表有三个单参数,因此会生成三条用例

image-20231017153208137

1.2 多参数化

多参数化相对于单参数,解释器参数传入多个,值需要传入列表-列表嵌套或列表-元组嵌套

@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+5", 7), ("7+5", 12)]) # 传多个参数,数值里列表需要再嵌套列表/元组
def test_mark_more(test_input, expected): # 形参与参数一一对应,参数与列表值一一对应
  assert eval(test_input) == expected  # eval()方法,将字符串转换成无字符串的表达式,如"3+5"转化成3+5并执行

多参数化mark.parametrize(参数…,值),格式比较宽松

@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+5", 7), ("7+5", 12)]) # 使用一个字符串包裹多个参数,值用列表-元组嵌套
@pytest.mark.parametrize("test_input","expected", [("3+5", 8), ("2+5", 7), ("7+5", 12)]) # 使用多个字符串表示参数,值用列表-元组嵌套
@pytest.mark.parametrize("test_input,expected", [["3+5", 8], ["2+5", 7], ["7+5", 12]]) # 使用一个字符串包裹多个参数,值用列表-列表嵌套

image-20231017153247384

1.3 参数重命名

image-20231017153258360

我们运行以上例子时,发现参数化的默认测试用例命名是以参数的值来命名的,不具有理解性,我们可以使用ids对参数化的用例进行命名,其中ids个数与传递的数据个数要完全一致

@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+5", 7), ("7+5", 12)],
                       ids=["number1", "number2", "number3"])
def test_mark_more(test_input, expected):
  assert eval(test_input) == expected  # eval()方法,将字符串转换成无字符串的表达式,如"3+5"转化成3+5并执行

命名之后运行

image-20231017153317595

image-20231017153323715

1.4 笛卡尔积

两个解释器参数化一起出现在一个用例上时,系统将对其进行笛卡尔积传递参数

image-20231017153337464

@pytest.mark.parametrize('wd', ['appium', 'selenium', 'pytest'])
@pytest.mark.parametrize('code', ['utf-8', 'gbk', 'gb2312'])
def test_dker(wd, code):
  print(f"wd:{wd},code:{code}")

image-20231017153348686

2.fixture实现参数化

测试代码的共享我们采用数据驱动的方式实现

数据驱动=数据局管理+参数化测试

接着以上的案例,我们可以发现代码重复很严重

image-20231017153408915

参数化测试: 一份代码,传递参数执行多个用例

fixture支持参数化,于是可以进行参数化测试

@pytest.fixture(
  params=[("https://baidu.com", "百度"),
          ("https://aliyun.com", "阿里"),
          ("https://qq.com", "腾讯")])
def test_data(request):
  return request.param  # 将参数化的参数返回给测试用例 
def test_ceshi(driver, test_data):
    url, tit = test_data  # 拆包 
    driver.get(url)
    title = driver.title
    assert tit in title
创建一个夹具test_data用于返回参数param,将参数化数据存放在fixture关键字params内,夹具test_data使用request关键字接收

![image-20231017153429354](http://wenxuanqiu.oss-cn-nanjing.aliyuncs.com/img/image-20231017153429354.png)

测试类使用fixture传入参数,当fixture的params为多参数时,需要对传入参数进行拆包;

这种通过将@pytest.fixture函数传递给测试方法的方式称为`pytest依赖注入`

简单实例:

```python
@pytest.fixture
def driver():
  """初始化浏览器驱动"""
  driver = webdriver.Chrome()
  yield driver
  driver.quit()

def test_example(driver):
  # `driver` 参数在调用时由 pytest 提供,并由 `driver` fixture 返回的实例赋值
  driver.get("https://www.example.com")
  assert "Example Domain" in driver.title

以上函数当中driver()会返回一个webdriver对象,将driver传入到test_example()方法当中,相当于给test_example()方法内所有的driver注入webdriver对象,具体实现步骤如下:

  1. 当 Pytest 运行 test_01_Check_autopatch_devices_monitor_view() 时,它会检查函数的参数。
  2. 发现 driver 这个参数时,Pytest 会去寻找一个名为 driverfixture,然后执行这个 fixture 的函数。
  3. 执行完 fixture 后,Pytest 会将 fixture 的返回值(即在这里是初始化好的浏览器驱动实例 driver)传递给测试函数 test_01_Check_autopatch_devices_monitor_view() 中的 driver 参数。

3.fixture传参和mark传参的区别

1.fixture是在fixture方法内直接传参,而mark是使用mark.parametrize关键字传参

2.fixture是一个全局方法,其他测试方法只需要调用fixture就可以使用参数化;而mark标签只定义在测试方法上,只能对某一测试用例参数化

3.多参数传值可以使用元组或列表,使用fixture传值需要拆包

image-20231017163857220

4.yaml数据参数化

4.1 yaml实现list
list
  - 10
  - 20
  - 30
4.2 yaml实现dict
dict
  by:id
  locator:name
  action:click
4.3 yaml嵌套
列表字典嵌套:
-
  - by:id
  - locator:name
  -action:click
  
对象列表字典嵌套:对象companiens:[{id:1,name:company1,price:200w},{id:2,name:company2,price:500w}]
companiens
  -
  	id:1
      name:company1
      price:200w
  -
  	id:2
      name:company2
      price:500w
4.4 加载yaml文件
# yaml文件加载
yaml.safe_load(open("./data.yaml"))

# pytest与yaml连用
@pytest.mark.parametrize(["a", "b"], yaml.safe_load(open("./data.yaml")))
def test_param(self, a, b):
  print(a + b)
4.5 导入yaml文件参数化传递数据

首先,我们先下载yaml插件

pip install pyyaml

也可以在python包中直接导入

image-20231017173349113

在项目里创建一个.yaml文件,使用yaml语法在文件中写好数据

image-20231017173455318

然后在测试类当中传入yaml文件路径,加载yaml文件

import pytest
import yaml

@pytest.mark.parametrize(["a", "b"], yaml.safe_load(open("./data.yaml"))) # 传入yaml文件路径并加载
def test_param(a, b):
  print(a + b)
4.6 yaml数据驱动案例

首先创建一个env.yml文件,内容为dict类型如下:

test: 127.0.0.1

在用例中我们引入env.yml文件判断文件内容

@pytest.mark.parametrize("env", yaml.safe_load(open("env.yml")))
def test_yaml(env):
  if "test" in env:
      print("这是测试环境")
  elif "dev" in env:
      print("这是开发环境")
  print(env)  # 只输出key值 test

输出结果:这是测试环境
test

输出env.yml全部内容

def test_case1(self):
  print(yaml.safe_load(open("env.yml"))) # test: 127.0.0.1

以上代码只读取到了key值test,而使用print(yaml.safe_load(open("env.yml")))打印输出的却是全部值,由此发现我们使用一个参数env读取dict类型yml文件时@pytest.mark.parametrize("env", yaml.safe_load(open("env.yml")))只能读取到第一个key值。

为了让我们能够读取dict类型的yml文件,我们可以在yml文件中使用列表包裹key-value,如下

-
test: 127.0.0.1

这个时候执行以上代码,就可以输出整个key-value数据

输出:

​ {‘test’: ‘127.0.0.1’}

我们对这个代码进行升级,采用列表嵌套的方式打印value值

@pytest.mark.parametrize("env", yaml.safe_load(open("env.yml")))
def test_yaml(env):
  if "test" in env:
      print("这是测试环境")
      print("测试环境的ip是:" + env["test"])
  elif "dev" in env:
      print("这是开发环境")
      print("开发环境的ip是:" + env["dev"])

输出结果:

测试环境的ip是:127.0.0.1

10、request 内置fixture

request:获取当前测试用例的相关信息

  • request.node (当前测试用例名称)
  • request.fixturenames (当前测试用例使用的fixture名称)
  • request.config.getoption(“htmlpath”) 生成的html测试报告

image-20231017153515005

11、pytest处理异常

image-20231017153524356

1.try…except捕获异常

image-20231017153531768

image-20231017153541007

2.pytest.raise()

pytest异常处理框架pytest.raise()底层还是封装了python的try…except方法

image-20231017153549564

image-20231017153554629

image-20231017153558192

12、pytest常见报错

1.pytest显示空套件

AttributeError: module ‘allure‘ has no attribute ‘severity_level‘

卸载allure-pytest

pip uninstall pytest-allure-adaptor

重新下载pytest-allure

pip insatll allure-pytest
Logo

一站式 AI 云服务平台

更多推荐