Python自动化之pytest框架使用详细解答
pytest是一个非常成熟的全功能的Python测试框架,主要有以下几个特点:
- 简单灵活,容易上手
- 支持参数化
- 能够支持简单的单元测试和复杂的功能测试,还可以用来做selenium/appnium等自动化测试、接口自动化测试(pytest+requests)
- pytest具有很多第三方插件,并且可以自定义扩展,比较好用的如pytest-selenium(集成selenium)、pytest-html(完美html测试报告生成)、pytest-rerunfailures(失败case重复执行)、pytest-xdist(多CPU分发)等
- 测试用例的skip和xfail处理
- 可以很好的和jenkins集成
- report框架----allure 也支持了pytest
- 安装 pip install -U pytest
2.查看版本
pytest --version
3.用例编写规范
-
- 测试文件以test_开头(以 _test结尾也可以)
- 测试类以Test开头,并且不能带init方法
- 测试函数以test_开头
- 断言使用基本的assert即可
运行参数
- 无参数
- 读取路径下符合条件的所有类、函数、方法全部执行
- -v
- 打印详细运行日志
- -s
- 打印print输出
- -k
- 跳过运行某个或某些用例
- pytest -k '类名'
- pytest -k '方法名
- pytest -k '类名 and not 方法名' #运行类里所有方法,不包含某个方法
- -x
- 遇到用例失败立即停止运行
- --maxfail
- 用例失败数达到某个设定的值停止运行
- pytest --maxfail=[num]
- -m
- 运行所有@pytest.mark.[标记名] 标记的用例
框架结构
与unittest类似,执行前后会执行setup,teardown来增加用例的前置和后置条件。pytest框架使用setup,teardown更为灵活,按照用例运行级别可以分为以下几类
- setup_module/teardown_module 模块级别,在模块始末调用
- setup_function/teardown_function 函数级别,在函数始末调用(在类外部)
- setup_class/teardown_class 类级别,每个类里面执行前后分别执行
- setup_method/teardown_method 方法级别,在方法始末调用(在类中)
- setup/teardown 方法级别,在方法始末调用(在类中)
调用顺序:setup_module > setup_class >setup_method > setup > teardown > teardown_method > teardown_class > teardown_module
for example:
#!/usr/bin/env python
# encoding: utf-8
'''
@Auther:chenshifeng
@version: v1.0
@file: test_calc.py
@time: 2020/9/14 9:39 PM
'''
# 测试文件
import sys, os
import pytest
sys.path.append(os.pardir)
from pythoncode.calc import Calculator
# 模块级别,在模块始末调用
def setup_module():
print('模块级别setup')
def teardown_module():
print('模块级别teardown')
# 函数级别,在函数始末调用(在类外部)
def setup_function():
print('函数级别setup')
def teardown_function():
print('函数级别teardown')
def test_case1():
print('testcase1')
class TestCalc:
# setup_class,teardown_class 类级别每个类里面执行前后分别执行
def setup_class(self):
self.cal = Calculator()
print('类级别setup')
</span><span style="color: rgba(0, 0, 255, 1)">def</span><span style="color: rgba(0, 0, 0, 1)"> teardown_class(self):
</span><span style="color: rgba(0, 0, 255, 1)">print</span>(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">类级别teardown</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 方法级别,每个方法里面的测试用例前后分别执行setup、teardown</span>
<span style="color: rgba(0, 0, 255, 1)">def</span><span style="color: rgba(0, 0, 0, 1)"> setup(self):
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> self.cal = Calculator()</span>
<span style="color: rgba(0, 0, 255, 1)">print</span>(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">setup</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">def</span><span style="color: rgba(0, 0, 0, 1)"> teardown(self):
</span><span style="color: rgba(0, 0, 255, 1)">print</span>(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">teardown</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 方法级别,每个方法里面的测试用例前后分别执行setup、teardown</span>
<span style="color: rgba(0, 0, 255, 1)">def</span><span style="color: rgba(0, 0, 0, 1)"> setup_method(self):
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> self.cal = Calculator()</span>
<span style="color: rgba(0, 0, 255, 1)">print</span>(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">方法级别setup</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">def</span><span style="color: rgba(0, 0, 0, 1)"> teardown_method(self):
</span><span style="color: rgba(0, 0, 255, 1)">print</span>(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">方法级别teardown</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">)
@pytest.mark.add
</span><span style="color: rgba(0, 0, 255, 1)">def</span><span style="color: rgba(0, 0, 0, 1)"> test_add1(self):
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> cal = Calculator()</span>
<span style="color: rgba(0, 0, 255, 1)">assert</span> 3 == self.cal.add(1, 2<span style="color: rgba(0, 0, 0, 1)">)
@pytest.mark.div
</span><span style="color: rgba(0, 0, 255, 1)">def</span><span style="color: rgba(0, 0, 0, 1)"> test_div(self):
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> cal = Calculator()</span>
<span style="color: rgba(0, 0, 255, 1)">assert</span> 1 == self.cal.div(1, 1)</span></pre>
运行结果如下
Testing started at 11:05 下午 ...
/usr/local/bin/python3.6 "/Applications/PyCharm CE.app/Contents/plugins/python-ce/helpers/pycharm/_jb_pytest_runner.py" --path /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest/testcode/test_calc.py
Launching pytest with arguments /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest/testcode/test_calc.py in /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest/testcode
============================= test session starts ==============================
platform darwin -- Python 3.6.4, pytest-6.0.2, py-1.9.0, pluggy-0.13.1 -- /usr/local/bin/python3.6
cachedir: .pytest_cache
rootdir: /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest, configfile: pytest.ini
collecting ... collected 3 items
test_calc.py::test_case1 模块级别setup
函数级别setup
PASSED [ 33%]testcase1
函数级别teardown
test_calc.py::TestCalc::test_add1 类级别setup
方法级别setup
setup
PASSED [ 66%]teardown
方法级别teardown
test_calc.py::TestCalc::test_div 方法级别setup
setup
PASSED [100%]teardown
方法级别teardown
类级别teardown
模块级别teardown
============================== 3 passed in 0.02s ===============================
Process finished with exit code 0
pytest参数化
Pytest是使用@pytest.mark.parametrize装饰器来实现数据驱动测试的
for example:
import pytest@pytest.mark.parametrize('a,b,result', [
(1, 1, 2),
(2, 3, 5),
(100, 200, 300)
])
def test_add(a, b, result):
cal = Calculator()
assert cal.add(a, b) == result
结果:
Testing started at 11:22 ... "D:\Program Files\Python\python.exe" "D:\Program Files\JetBrains\PyCharm Community Edition 2020.2.1\plugins\python-ce\helpers\pycharm\_jb_pytest_runner.py" --target test_calc.py::test_add Launching pytest with arguments test_calc.py::test_add in D:\chenshifeng\mycode\Python\test_pytest\testing============================= test session starts =============================
platform win32 -- Python 3.7.4, pytest-6.0.2, py-1.9.0, pluggy-0.13.1 -- D:\Program Files\Python\python.exe
cachedir: .pytest_cache
rootdir: D:\chenshifeng\mycode\Python, configfile: pytest.ini
collecting ... collected 3 itemstest_calc.py::test_add[1-1-2] PASSED [ 33%]
test_calc.py::test_add[2-3-5] PASSED [ 66%]
test_calc.py::test_add[100-200-300] PASSED [100%]============================== 3 passed in 0.03s ==============================
Process finished with exit code 0
修改结果显示名称
通过上面的运行结果,我们可以看到,为了区分参数化的运行结果,在结果中都会显示数据组合而成的名称。
数据短小还好说,如果数据比较长而复杂的话,那么就会很难看。
@pytest.mark.parametrize()
还提供了第三个 ids
参数来自定义显示结果。
import pytest# 参数化
@pytest.mark.parametrize('a,b,result', [
(1, 1, 2),
(2, 3, 5),
(100, 200, 300)
], ids=['int0', 'int1', 'int2']) # 修改结果显示名称
def test_add(a, b, result):
cal = Calculator()
assert cal.add(a, b) == result
结果:
============================= test session starts ============================= platform win32 -- Python 3.7.4, pytest-6.0.2, py-1.9.0, pluggy-0.13.1 -- D:\Program Files\Python\python.exe cachedir: .pytest_cache rootdir: D:\chenshifeng\mycode\Python, configfile: pytest.ini collecting ... collected 3 itemstest_calc.py::test_add[int0] PASSED [ 33%]
test_calc.py::test_add[int1] PASSED [ 66%]
test_calc.py::test_add[int2] PASSED [100%]============================== 3 passed in 0.03s ==============================
Process finished with exit code 0
pytest fixtures
fixture用途
pytest fixture 与setup,teardown功能一样,但比之更加灵活,完全可以代替setup,teardown
1.做测试前后的初始化设置,如测试数据准备,链接数据库,打开浏览器等这些操作都可以使用fixture来实现
2.测试用例的前置条件可以使用fixture实现
3.支持经典的xunit fixture ,像unittest使用的setup和teardown
4.fixture可以实现unittest不能实现的功能,比如unittest中的测试用例和测试用例之间是无法传递参数和数据的,但是fixture却可以解决这个问题
fixture定义
fixture通过@pytest.fixture()装饰器装饰一个函数,那么这个函数就是一个fixture
#!/usr/bin/python
# -*- coding: UTF-8 -*-
"""
@author:chenshifeng
@file:test_login.py
@time:2020/09/15
"""
import pytest
@pytest.fixture()
def login():
print('登陆方法')
yield #激活fixture teardown方法
print('teardown')
# 测试用例之前,先执行login方法
def test_case1(login):
print('case1')
def test_case2():
print('case2')
def test_case3():
print('case3')
运行结果如下:
test_login.py::test_case2
test_login.py::test_case3
============================== 3 passed in 0.02s ===============================
Process finished with exit code 0
登陆方法
PASSED [ 33%]case1
teardown
PASSED [ 66%]case2
PASSED [100%]case3
fixture作用范围(scope)
fixture里面有个scope参数可以控制fixture的作用范围:session > module > class > function
- - function 每一个函数或方法都会调用,默认为function
- - class 每一个类调用一次,一个类可以有多个方法
- - module,每一个.py文件调用一次,该文件内又有多个function和class
- - session 是多个文件调用一次,可以跨.py文件调用,每个.py文件就是module
cope='function'
#!/usr/bin/python # -*- coding: UTF-8 -*- """ @author:chenshifeng @file:test_login.py @time:2020/09/15 """ import pytest@pytest.fixture() #默认为scope='function'
def login():
print('登陆方法')
yield ['username','passwd'] #激活fixture teardown方法
print('teardown')# 测试用例之前,先执行login方法
def test_case1(login):
print(f'case1 login={login}')def test_case2(login):
print('case2')def test_case3(login):
print('case3')
运行结果如下:
Testing started at 12:02 上午 ... /usr/local/bin/python3.6 "/Applications/PyCharm CE.app/Contents/plugins/python-ce/helpers/pycharm/_jb_pytest_runner.py" --path /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest/testcode/test_login.py Launching pytest with arguments /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest/testcode/test_login.py in /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest/testcode============================= test session starts ==============================
platform darwin -- Python 3.6.4, pytest-6.0.2, py-1.9.0, pluggy-0.13.1 -- /usr/local/bin/python3.6
cachedir: .pytest_cache
rootdir: /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest, configfile: pytest.ini
collecting ... collected 3 itemstest_login.py::test_case1
test_login.py::test_case2
test_login.py::test_case3============================== 3 passed in 0.02s ===============================
Process finished with exit code 0
登陆方法
PASSED [ 33%]case1 login=['username', 'passwd']
teardown
登陆方法
PASSED [ 66%]case2
teardown
登陆方法
PASSED [100%]case3
teardown
scope='module'
#!/usr/bin/python # -*- coding: UTF-8 -*- """ @author:chenshifeng @file:test_login.py @time:2020/09/15 """ import pytest@pytest.fixture(scope='module') #默认为scope='function'
def login():
print('登陆方法')
yield ['username','passwd'] #激活fixture teardown方法
print('teardown')# 测试用例之前,先执行login方法
def test_case1(login):
print(f'case1 login={login}')def test_case2(login):
print('case2')def test_case3(login):
print('case3')
结果:
Testing started at 12:08 上午 ... /usr/local/bin/python3.6 "/Applications/PyCharm CE.app/Contents/plugins/python-ce/helpers/pycharm/_jb_pytest_runner.py" --path /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest/testcode/test_login.py Launching pytest with arguments /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest/testcode/test_login.py in /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest/testcode============================= test session starts ==============================
platform darwin -- Python 3.6.4, pytest-6.0.2, py-1.9.0, pluggy-0.13.1 -- /usr/local/bin/python3.6
cachedir: .pytest_cache
rootdir: /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest, configfile: pytest.ini
collecting ... collected 3 itemstest_login.py::test_case1
test_login.py::test_case2
test_login.py::test_case3============================== 3 passed in 0.02s ===============================
Process finished with exit code 0
登陆方法
PASSED [ 33%]case1 login=['username', 'passwd']
PASSED [ 66%]case2
PASSED [100%]case3
teardown
fixture自动调用(autouse=True)
autouse设置为True,自动调用fixture功能,无需额外继承
#!/usr/bin/python # -*- coding: UTF-8 -*- """ @author:chenshifeng @file:test_search.py @time:2020/09/16 """import pytest
@pytest.fixture(autouse=True) # 默认为scope='function'
def login():
print('登陆方法')
yield ['username', 'passwd'] # 激活fixture teardown方法
print('teardown')def test_search1(): #无需继承login
print('搜索用例1')def test_search2():
print('搜索用例2')
结果:
============================= test session starts ============================== platform darwin -- Python 3.6.4, pytest-6.0.2, py-1.9.0, pluggy-0.13.1 -- /usr/local/bin/python3.6 cachedir: .pytest_cache rootdir: /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest, configfile: pytest.ini collecting ... collected 2 itemstest_search.py::test_search1 登陆方法
PASSED [ 50%]搜索用例1
teardowntest_search.py::test_search2 登陆方法
PASSED [100%]搜索用例2
teardown============================== 2 passed in 0.01s ===============================
Process finished with exit code 0
fixture参数化(params)
@pytest.fixture有一个params参数,接受一个列表,列表中每个数据都可以作为用例的输入。也就说有多少数据,就会形成多少用例。可以通过'''request.param'''来获取该次调用的参数
#!/usr/bin/python # -*- coding: UTF-8 -*- """ @author:chenshifeng @file:test_login.py @time:2020/09/15 """ import pytest@pytest.fixture(params=['user1', 'user2', 'user3'])
def login(request):
print('登陆方法')
print('传入的参数为:'+request.param) # 获取params参数
yield ['username', 'passwd'] # 激活fixture teardown方法
print('teardown')# 测试用例之前,先执行login方法
def test_case1(login):
print(f'case1 login={login}')def test_case2(login):
print('case2')def test_case3(login):
print('case3')
结果:
============================= test session starts ============================== platform darwin -- Python 3.6.4, pytest-6.0.2, py-1.9.0, pluggy-0.13.1 -- /usr/local/bin/python3.6 cachedir: .pytest_cache rootdir: /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest, configfile: pytest.ini collecting ... collected 9 itemstest_login.py::test_case1[user1]
test_login.py::test_case1[user2]
test_login.py::test_case1[user3]
test_login.py::test_case2[user1]
test_login.py::test_case2[user2]
test_login.py::test_case2[user3]
test_login.py::test_case3[user1] 登陆方法
传入的参数为:user1
PASSED [ 11%]case1 login=['username', 'passwd']
teardown
登陆方法
传入的参数为:user2
PASSED [ 22%]case1 login=['username', 'passwd']
teardown
登陆方法
传入的参数为:user3
PASSED [ 33%]case1 login=['username', 'passwd']
teardown
登陆方法
传入的参数为:user1
PASSED [ 44%]case2
teardown
登陆方法
传入的参数为:user2
PASSED [ 55%]case2
teardown
登陆方法
传入的参数为:user3
PASSED [ 66%]case2
teardown
登陆方法
传入的参数为:user1
PASSED [ 77%]case3
teardowntest_login.py::test_case3[user2] 登陆方法
传入的参数为:user2
PASSED [ 88%]case3
teardowntest_login.py::test_case3[user3] 登陆方法
传入的参数为:user3
PASSED [100%]case3
teardown============================== 9 passed in 0.06s ===============================
Process finished with exit code 0
参数化与fixture结合(indirect=True)
#!/usr/bin/python # -*- coding: UTF-8 -*- """ @author:chenshifeng @file:test_cart.py @time:2020/09/16 """ import pytest@pytest.fixture(params=['user1', 'user2', 'user3'])
def login(request):
print('登陆方法')
print('传入的参数为:' + str(request.param)) # 获取params参数
yield ['username', 'passwd'] # 激活fixture teardown方法
print('teardown')
# 参数化结合fixture使用
情况一:传入值和数据
情况二:传入一个fixture方法,将数据传入到fixture方法中,fixture使用request参数来接受这组数据,在方法体中使用request.param来接受这个数据
@pytest.mark.parametrize('login', [
('username1', 'passwd1'),
('username2', 'passwd2')
], indirect=True)
def test_cart3(login):
print('购物车用例3')
结果:
============================= test session starts ============================== platform darwin -- Python 3.6.4, pytest-6.0.2, py-1.9.0, pluggy-0.13.1 -- /usr/local/bin/python3.6 cachedir: .pytest_cache rootdir: /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest, configfile: pytest.ini collecting ... collected 2 itemstest_cart.py::test_cart3[login0]
test_cart.py::test_cart3[login1]============================== 2 passed in 0.02s ===============================
Process finished with exit code 0
登陆方法
传入的参数为:('username1', 'passwd1')
PASSED [ 50%]购物车用例3
teardown
登陆方法
传入的参数为:('username2', 'passwd2')
PASSED [100%]购物车用例3
teardown
conftest.py
1.conftest.py文件名字是固定的,不可以做任何修改
2.文件和用例文件在同一个目录下,那么conftest.py作用于整个目录
3.conftest.py文件不能被其他文件导入
4.所有同目录测试文件运行前都会执行conftest.py文件
#!/usr/bin/python # -*- coding: UTF-8 -*- """ @author:chenshifeng @file:conftest.py @time:2020/09/15 """import pytest
@pytest.fixture(params=['user1', 'user2', 'user3'])
def login(request):
print('登陆方法')
print('传入的参数为:'+str(request.param)) # 获取params参数
yield ['username', 'passwd'] # 激活fixture teardown方法
print('teardown')
#!/usr/bin/python # -*- coding: UTF-8 -*- """ @author:chenshifeng @file:test_login.py @time:2020/09/15 """# 测试用例之前,先执行login方法
import pytestdef test_case1(login):
print(f'case1 login={login}')@pytest.mark.usefixtures('login')
def test_case2():
print('case2')
# print(f'case1 login={login}') #该方法无法获取返回值def test_case3(login):
print('case3')
运行test_login.py文件,结果如下
============================= test session starts ============================== platform darwin -- Python 3.9.0, pytest-6.1.1, py-1.9.0, pluggy-0.13.1 -- /usr/local/bin/python3.9 cachedir: .pytest_cache rootdir: /Users/chenshifeng/MyCode/PythonCode/SFDSZL/test_pytest, configfile: pytest.ini plugins: allure-pytest-2.8.18 collecting ... collected 9 itemstest_login.py::test_case1[user1]
test_login.py::test_case1[user2]
test_login.py::test_case1[user3]
test_login.py::test_case2[user1]
test_login.py::test_case2[user2]
test_login.py::test_case2[user3]
test_login.py::test_case3[user1] 登陆方法
传入的参数为:user1
PASSED [ 11%]case1 login=['username', 'passwd']
teardown
登陆方法
传入的参数为:user2
PASSED [ 22%]case1 login=['username', 'passwd']
teardown
登陆方法
传入的参数为:user3
PASSED [ 33%]case1 login=['username', 'passwd']
teardown
登陆方法
传入的参数为:user1
PASSED [ 44%]case2
teardown
登陆方法
传入的参数为:user2
PASSED [ 55%]case2
teardown
登陆方法
传入的参数为:user3
PASSED [ 66%]case2
teardown
登陆方法
传入的参数为:user1
PASSED [ 77%]case3
teardowntest_login.py::test_case3[user2] 登陆方法
传入的参数为:user2
PASSED [ 88%]case3
teardowntest_login.py::test_case3[user3] 登陆方法
传入的参数为:user3
PASSED [100%]case3
teardown============================== 9 passed in 0.04s ===============================
Process finished with exit code 0
转载至https://www.cnblogs.com/feng0815/p/13676226.html