配置环境:
Python:3.11.7
pytest:8.3.2
基本调用
在使用之前,我们需要了解pytest的默认使用限制,py文件需要以'test_'开头或'_test'结尾,类也需要以'Test'开头,且不能带init方法,其中方法也需要以'test_'开头,函数需要以'test_'开头。否则pytest会直接忽略,不运行不合规定的函数方法。
下面是一个基本的例子:
import pytest
def test_a():
print('test_a++++++++++++++++++++')
return 1 * 0
def test_b():
print('test_b++++++++++++++++++++')
return 1 / 0
# -s 表示将测试函数中的print打印出来,如果没有-s,print将不会打印
if __name__ == '__main__':
pytest.main(['-s'])
运行结果:
pytest配置文件
因为pytest的命名规则,导致写代码很麻烦,所有我们可以通过修改配置文件来添加我们需要的东西。
pytest的配置文件为pytest.ini,将文件放置在测试目录下,在运行时pytest会自动读取配置文件,达到我们想要的效果。
addopts使用比较频繁,它可以更改pytest的运行次数、输出方式等等,类似于Linux的指令。
其中python_* 对应着运行文件名、类名。方法或者函数名,我们可以通过添加命名规则来匹配更多的函数。
[pytest]
: ini文件中,英文分号后面的内容都是注释
: 添加参数,通常是- 或 -- 开头
: --html表示将测试报告以html文档输出保存,=后面为保存路径
: --reruns 表示失败后再运行几次
addopts = -s --html=./report.html --reruns 5
: 测试模块所在目录
testpaths = ./scripts
: 测试模块文件名称规则 多个内容空格分隔
python_files = test_*.py *_test.py
: 测试类名规则
python_classes = Test_*
: 测试函数或者方法的名称规则
python_functions = test_*
下面是一个小例子:
配置文件:
[pytest]
addopts = -s
testpaths = ./
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = demo_* test_*
代码:
import pytest
def test_a():
print('test_a++++++++++++++++++++')
return 1 * 0
def test_b():
print('test_b++++++++++++++++++++')
return 1 / 0
def demo_a():
print('demo_a++++++++++++++++++++')
return 1 * 0
def demo_b():
print('demo_b++++++++++++++++++++')
return 1 / 0
def demo_c():
print('demo_c++++++++++++++++++++')
return 1 * 0
if __name__ == '__main__':
pytest.main(['-s'])
运行结果:
可以看到,即使是没有按照默认要求命名的demo函数,在更改配置文件后也能运行。
mark 标记功能
在测试运行时,如果有想要跳过的函数,或者已经认为是错误的函数,我们都可以将其标记,pytest在遇到他们时,就会打上对应的标签,会选择直接跳过。
# 跳过该函数
@pytest.mark.skip()
# 将该函数标记为错误,可以添加raises表示错误类型
@pytest.mark.xfail(raises=ZeroDivisionError)
实例代码:
import pytest
def test_a():
print('test_a++++++++++++++++++++')
return 1 * 0
# 使用mark来定义该函数为错误
@pytest.mark.xfail(raises=ZeroDivisionError)
def test_b():
print('test_b++++++++++++++++++++')
return 1 / 0
def demo_a():
print('demo_a++++++++++++++++++++')
return 1 * 0
# 使用mark来跳过测试
@pytest.mark.skip(reason='跳过')
def demo_b():
print('demo_b++++++++++++++++++++')
return 1 / 0
def demo_c():
print('demo_c++++++++++++++++++++')
return 1 * 0
if __name__ == '__main__':
pytest.main(['-s'])
运行结果:
可以看到,原本两个错误的函数,一个被标记为了跳过,一个被标记为了错误。
参数化
mark
使用mark直接传入参数,函数不用调用其他的函数。
import pytest
# 使用mark进行传参,实现参数化
@pytest.mark.parametrize(['a', 'b'], [(1, 2), (30, 30), (37, 92)])
def test_a(a, b):
print('test__________________')
# 函数断言
assert a + b > 100
if __name__ == '__main__':
pytest.main()
运行结果:
fixture
通过fixture定义函数,在后面的函数中用参数的方式调用fixture函数,即可将fixture函数的返回值当做参数调用。
import pytest
@pytest.fixture()
def account():
a = 'account'
return a
def test_1(account):
print(account)
if __name__ == '__main__':
pytest.main()
运行结果:
fixture装饰器
pytest的夹具
夹具一般在函数的前后进行运行。在函数运行前搭建函数需要的环境、参数,在函数运行后回收资源。
pytest的默认夹具命名规则十分死板,为setup和teardown,其分为了模块(model),类(class),函数(function),方法(method)。
import pytest
# 夹具 用于初始化环境和回收资源,有模块化(model),类(class),函数(function),方法(method)
# 模块化夹具---------------
def setup_module(args):
print('set_module-----------------', args)
def teardown_module(args):
print('teardown_module-----------', args)
# ---------------------------
# 函数化夹具---------------
def setup_function(args):
print('setup function---------------')
def teardown_function(args):
print('teardown function---------------')
# --------------------------
def test_func_1():
print('function11fffffffffffffffffff')
def test_func_2():
print('function22uuuuuuuuuuuuuuuuuu')
class Test_1():
# 类级夹具---------------------
def setup_class(self):
print('setup class -----------------')
def teardown_class(self):
print('teardown class-----------------')
# -----------------------------------
# 方法类夹具--------------------------
def setup_method(self):
print('setup method------------------')
def teardown_method(self):
print('teardown method------------------')
# ----------------------------------
def test_1(self):
print('test1111111111111111111111')
def test_2(self):
print('test222222222222222222222')
if __name__ == '__main__':
pytest.main(['-s'])
运行结果:
============================= test session starts =============================
collecting ... collected 4 items
test_3.py::test_func_1 set_module----------------- <module 'test_3' from 'F:\\自动化测试\\pytest\\test_3.py'>
setup function---------------
function11fffffffffffffffffff
PASSEDteardown function---------------
test_3.py::test_func_2 setup function---------------
function22uuuuuuuuuuuuuuuuuu
PASSEDteardown function---------------
test_3.py::Test_1::test_1 setup class -----------------
setup method------------------
test1111111111111111111111
PASSEDteardown method------------------
test_3.py::Test_1::test_2 setup method------------------
test222222222222222222222
PASSEDteardown method------------------
teardown class-----------------
teardown_module----------- <module 'test_3' from 'F:\\自动化测试\\pytest\\test_3.py'>
======================== 4 passed, 1 warning in 0.02s =========================
使用fixture定义夹具
因为pytest的默认夹具命名规则原因,使用是十分不方便,所以我们可以用fixture来定义夹具。
fixture的调用方式:
@pytest.fixture(scope = "function",params=None,autouse=False,ids=None,name=None)
scope的使用
scop的参数决定该夹具的使用范围,具体参数有函数级(function),类级(class),模块级(module)和会话级(session)。
函数级
函数级用于函数或方法的导入,下面是使用方法。
参数直接传入:
import pytest
# fixture定义夹具
# function / class / model / session
# function
@pytest.fixture()
def before():
print('第一层fixture')
a = 'before'
return a
# 二次调用
@pytest.fixture()
def login(before):
print('第二层fixture')
return before
# 调用函数before
@pytest.mark.usefixtures('before')
def test_1():
print('test_1')
# 直接调用函数login
def test_2(login):
print('test_2',login)
if __name__ == '__main__':
pytest.main(['-s'])
运行效果:
注意:function可以进行二次调用,即fixture函数'b'可以调用另一个fixture函数'a',将函数'a'看做一个参数即可,但'b'函数不会自动将'a'函数return的值进行return,'b'函数需要自己去return一个值或将'a'函数进行return,例如例子中login函数return了before函数。
类级
类级的范围是一个class,在一个class中只运行一次,fixture函数可以在类中延迟导入,实现在某些方法不需要fixture函数时先不导入,需要fixture时再导入。
import pytest
@pytest.fixture(scope='class')
def before():
print('class fixture-----------')
a = 'before'
return a
class Test1():
def test_1(self):
print('11111111111111111')
def test_2(self,before):
print('22222222222',before)
def test_3(self,before):
print('33333333333333',before)
def test_4(self):
print('44444444444444')
if __name__ == '__main__':
pytest.main(['-s'])
运行结果:
我在test_2才导入了before函数,所以在运行test_2时fixture函数才运行,而test_3虽然也导入了before函数,但是before函数已经在该class中运行了,就不会重复运行,但是其中的参数依然是生效的。
模块级
覆盖范围为整个py文件,在导入时生效。
import pytest
@pytest.fixture(scope='module')
def before():
print('class fixture-----------')
a = 'before'
return a
def test_1():
print('test_1')
def test_2(before):
print('test_2',before)
def test_3(before):
print('test_3',before)
def test_4():
print('test_4')
class Test1():
def test_5(self):
print('test5')
def test_6(self,before):
print('test6',before)
def test_7(self,before):
print('test7',before)
def test_8(self):
print('test8')
if __name__ == '__main__':
pytest.main(['-s'])
运行结果:
和class类似,首次调用后,在py文件中只运行一次,但效果覆盖整个文件。
会话级
回话级的覆盖范围为多个py文件,在只对一个py文件使用时,效果类似于module,所以一般在py配置文件conftest.py中。
conftest.py文件类似于pytest的配置文件,pytest在运行时会先运行conftest.py文件,生成全局fixture函数。conftest一般放在同级文件夹或父级文件夹中,以便能覆盖py文件。
params的使用
import pytest
@pytest.fixture(params=[1,2,3])
def init_data(request):
print('request参数是',request.param)
return request.param
# 尝试调用函数
def test_data(init_data):
assert init_data>2
if __name__ == '__main__':
pytest.main(['-s'])
运行效果:
params类似于一个for循环的参数,在函数导入fixture函数后,fixture函数会依次读取列表中的值给函数,params的list有多长,fixture就传几次参数,函数就循环运行几次。
params在使用时,需要添加request模块,通过request.param对params的值进行输出。
autouse的使用
autouse默认为false,为true时表示自动调用,无需传入参数名。
import pytest
@pytest.fixture()
def before():
print('第一层函数')
a = 'before'
return a
@pytest.fixture(autouse=True)
def login():
print('login')
def test_1(before):
print(before)
@pytest.mark.usefixtures('before')
def test_2():
pass
if __name__ == '__main__':
pytest.main(['-s'])
运行效果:
从图中可以看出,autouse=True的运行优先级更高,会在autouse=False函数之间运行,这里我也使用了两种不同的调用方式:装饰器'@pytest.mark.usefixture'和直接用参数引用,使用装饰器无法使用fixture函数的输出,但能够运行fixture函数来创建环境,而直接引用参数就可以使用fixture函数的输出并将其使用。
ids的使用
ids一般与params搭配使用,主要是给每个进程取一个标识,我们可以看看效果:
没有使用ids:
import pytest
@pytest.fixture(params=[4,[2,3],(9,2),{3,6}])
def init_data(request):
print('request参数是',request.param)
return request.param
# 尝试调用函数
def test_data(init_data):
assert init_data == [2,3]
if __name__ == '__main__':
pytest.main(['-s'])
运行结果:
加入ids:
import pytest
@pytest.fixture(params=[4,[2,3],(9,2),{3,6}],ids=['4','[2,3]','(9,2)','{3,6}'])
def init_data(request):
print('request参数是',request.param)
return request.param
# 尝试调用函数
def test_data(init_data):
assert init_data == [2,3]
if __name__ == '__main__':
pytest.main(['-s'])
运行效果:
name的使用
默认情况下,fixture会将函数名作为参数名来传递调用,我们也可以通过name参数来重命名参数名。如果使用了name,那么原本的函数名将会被替换,函数名便不能被调用。
import pytest
@pytest.fixture(name='login')
def before():
a = 'before'
return a
def test_1(login):
print(login)
def test_2(before):
print(before)
if __name__ == '__main__':
pytest.main(['-s'])
运行结果:
pytest插件
HTML报告
通过在配置文档中加入'--html=./report.html',可在当前目录下生成名为report.html的网页报告,更加方便的查看运行结果。
在使用前,我们需要下载对应的库,在命令行输入'pip install pytest-html'即可下载,然后在配置文件中的addpots后面添加'--html=文件保存路径/report.html'即可。
失败重试
在有外部依赖是,代码运行错误不一定是代码问题,此时我们可以对没有通过的代码进行多次运行。使用pytest-rerunfailures即可实现。
pip install pytest-rerunfailures
在配置文件的addpots中,添加'--reruns 重复运行的次数'即可。