pytest是一个单测框架;能对定义的测试对象进行测试,并且可以针对测试对象进行前后置处理,对测试结果进行断言,对执行过程中的异常进行干预,并且可以收集测试结果以生成xml格式的测试报告
大纲
1 | 原理 |
原理
pytest是一套单测框架;有着自己的巡检方法,他会自动查找整个包中以test开头/结尾的模块,并查找其中test开头/结尾的方法/类(类很特殊必须要以Test开头,且类中不能有__init__方法),全部保存在内存中,在触发执行时,会根据调度指令来确定自动执行的用例范围,来进行执行; 同时执行用例时做了装饰器操作,将用例作为参数传递到pytest框架中执行。
使用场景
- 用例自动单测场景: 如接口自动化、UI自动化、各类用例场景;
- 通用前后置处理场景: 如数据库操作前的建立连接,操作完成后的断开连接;
- 参数化操作场景: 如多设备操作、各种UI操作、各种接口操作;
如何使用
分为:调度/执行、前后置处理(固件)、用例(断言+异常处理+跳过+标记)、参数化、定制报告、性能(缓存操作)
调度/执行
调度有两种方式:命令行、pytest.main()
pytest.main():
实际也是调用命令行来进行的操作,我们程序一般使用pytest.main()来调度;注意pytest.main(['-m','对象']);第一个参数是选项,第二个参数是对象;pytest.main()会将当前模块加入缓存,因此不建议执行过程中调用多次,最好只调用一次,示例如下。
- pytest.main([“-qq”], plugins=[MyPlugin()]) //指定一个插件运行
- pytest.main([‘-sq’,’test_nodeid.py::TestNodeId::test_two[1-1]’])
命令行调用:
pytest 各选项如-rx 对象如test_1.py::test_fun
1
2
3
4
5
6
7
8
9
10
11对象:可通过命令行执行特定包内的用例/特定目录下/特定模块/特定方法/特定方法的特定参数化参数
pytest –pyargs pkg.testing
pytest testing/pytest test_mod.py/pytest test_mod.py::test_fun
pytest -q -s test_nodeid.py::TestNodeId::test_two[1-1]
模糊匹配:执行含特定字符或(也可以是和)不含特定字符的模块/类/函数:
pytest -k “_class and/or not two”
还可以为用例添加标记。从而指定特定标记的用例:pytest -m 标记名
@pytest.mark.标记名
def test_one()pytest自动巡检机制:
pytest会自动化去查找到测试用例,他是按模块的首尾是否是test来判断的,模块中的类(类很特殊必须要以Test开头,且类中不能有__init__方法)/方法首尾是test则判定为测试用例,这些测试用例维护在内存中,以数组的形式维护,触发执行时会自动执行这些用例;用例执行顺序是按测试用例的名称从大到小来的,可以利用pytest-ordering插件来定制用例的执行顺序; 默认情况下执行pytest.main()时仅执行当前模块的用例,如果需要执行其他路径下的用例,可以指定路径。
常用命令行选项
- 查看结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29pytest -q //-q选项可以查看精简版的测试报告
pytest -r //(同-q只是可以过滤)显示精简的总结报告;-r后还要加一个参数,表示需要显示的内容,其他的会过滤,如-rfs(f是失败的,s是跳过的,显示失败的和跳过的实例)
pytest -s //-s是--capture=no的快捷方式,是用来关闭输出(标准输出/标准错误)捕获,使得可以看到print()输出的内容
pytest --capture=fd(默认捕获行为) //指定只捕获文件/接口层面的标准输出/标准错误
pytest --capture=sys //指定只捕获系统级别的标准输出/标准错误
pytest -l, --showlocals # 打印本地变量
pytest --tb=auto # 默认模式
pytest --tb=long # 尽可能详细的输出
pytest --full-trace //比--tb=long更详细的输出模式
pytest --tb=short # 更简短的输出
pytest --tb=line # 每个失败信息总结在一行中
pytest --tb=native # python的标准输出
pytest --tb=no # 不打印失败信息
pytest --pdb //失败时进入pdb诊断器模式
pytest --trace //启动时就进入pdb诊断器模式
pytest --durations=10 //获取执行最慢的十个测试用例
pytest -vv //获取用例的执行时间
pytest --show-capture //定义是否显示,标准输出、标准错误、或异常堆栈;支持的选项值有[no,stdout,stderr,log,all,默认为all]
pytest --assert=plain选项 //关闭断言失败提示信息
python -p no:faulthandler //默认是打开的,可以通过此命令关闭回溯信息当用例执行出现断错误或超时,faulthandler模块,可以展示Python脚本的回溯信息(回溯不是错误信息,此时程序也不会中断,只是打印此时的相关信息,类似监控);
faulthandler_timeout=5 //(pytest.ini文件配置)可以配置超时时间,一旦超时则显示所有线程的回溯信息
pytest -o faulthandler_timeout=X //命令行配置超时时间
pytest --disable-warnings //命令行选项来阻止,告警信息的显示(有捕获告警只是不显示)
pytest -p no:warnings //命令行,直接禁止告警的捕获;
pytest -q -s --collect-only test_params.py //collect-only仅显示测试ID,不执行用例(用来收集测试用例的名称)
设置固件的测试ID:使用ids @pytest.fixture(params=[0, 1], ids=['spam', 'ham']),也可以用一个函数来动态生成测试ID@pytest.fixture(params=[0, 1], ids=fun());
同理也可以设置用例的测试ID:@pytest.mark.parametrize(argnames, argvalues, indirect=False, ids=None, scope=None) 参数化时ids是每个测试的实例的测试ID,报告可以记录,默认值是每一个实例的argvalue_拼接,如上是1_2;手动定义时ids值长度必须和argvalue长度保持一致;默认所有的id都进行了ascii编码,可以在pytest.ini中禁用ascii编码
(disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True) - 执行
1
2
3
4
5
6
7
8
9pytest :: //执行指定的模块方法/类
pytest path //执行指定路径及子路径下的所有以test开头或结尾的模块中,以test开头或结尾的方法/类
pytest -k //模糊匹配要执行的用例
pytest -m //执行指定标记用例
pytest -x //pytest遇到第一个失败时退出执行
pytest --maxfail==2 //设定允许失败的最大次数
pytest -p mypluginmodule //-p尽早加载插件,插件可以是本地的插件,也可以是公共插件
pytest -p no:doctest //加no提前阻止插件的加载 - 结果
1
2
3pytest --junitxml=path //在path路径生成一个xml格式的测试报告
junit_suite_name = 节点名 //(pytest.ini中)自定义XML文件中testsuite根节点的名称
junit_duration_report = call //(pytest.ini)报告只记录测试用例时间,不含前后置时间,默认time属性是含所有时间的
- 查看结果
固件-前后置处理
固件的作用是提供了一个固定的基线,能重复的运行;固件装饰器(@pytest.fixture)的作用是注册一个固件,测试用例可以通过显示调用的方式使用固件(固件作为入参),调用的是固件的形参,实际生效的是固件的返回对象作为入参;默认可以不调用固件,因为固件默认配置了自动调用
作用:固件的作用主要是进行用例的前后置处理
固件根本作用域的不同,可以分别针对session/包/模块/类/方法进行前后置处理
前后置处理
- 实现示例:前后置处理有三种实现方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26#前后置处理方法1-yield
@pytest.fixture()
def smtp_connection_yield():
smtp_connection = smtplib.SMTP("smtp.163.com", 25, timeout=5)
yield smtp_connection
print("关闭SMTP连接")
smtp_connection.close()
#前后置处理方法2-利用上下文自动关闭对象
@pytest.fixture()
def smtp_connection_yield():
with smtplib.SMTP("smtp.163.com", 25, timeout=5) as smtp_connection:
yield smtp_connection
#利用注册的方式,来注册后置处理
@pytest.fixture()
def smtp_connection_fin(request):
smtp_connection = smtplib.SMTP("smtp.163.com", 25, timeout=5)
def fin():
smtp_connection.close()
request.addfinalizer(fin)
return smtp_connection
//(这个后置操作,只能对前置产生的实例对象生效)
- 实现示例:前后置处理有三种实现方式:
固件的使用
- 放置位置: 固件一般是放置在conftest.py模块中统一维护;当然也可以在用例模块中定义固件;固件是支持显示调用(作为测试用例的入参)和自动调用的(默认就支持@pytest.fixture(autouse=True)autouse=True可省略)
- 作用域
固件一般放置在conftest.py模块中,而此模块一般放置根目录/功能目录的顶层(可以放置多个此模块,按就近原则生效,也就是在用例当前目录找到固件后就不去上一级查找);可以为每个固件定义作用域,定义完后,固件分别在作用域的范围内执行,默认的作用域是@pytest.fixture(scope='function'),作用域主要决定的是执行次数和执行顺序
支持的作用域有:@pytest.fixture(scope=’function/class/module/session/package’,autouse=True)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24@pytest.fixture(scope='function') //每个用例方法都执行一遍固件的前后置处理
@pytest.fixture(scope='class') //每个用例类仅执行一遍固件的前后置处理
@pytest.fixture(scope='module') //每个模块仅执行一遍固件的前后置处理
@pytest.fixture(scope='session') //每个会话对象仅执行一遍固件的前后置处理
@pytest.fixture(scope='package') //每个包仅执行一遍固件的前后置处理
类显示的调用固件:@pytest.mark.usefixtures('固件名','第二个固件'),此装饰器也可以给fun使用;
模块显示的调用固件:test=pytest.mark.usefixtures("transact")
可以使用固件来生成工厂函数,从而实现模块化,和函数定制(此时固件是可以接受参数的);
固件可以进行参数化:如下,固件进行参数化后会创建多个实例,因此使用这些实例的测试用例也会执行多次
@pytest.fixture(params=[('3+5', 8),pytest.param(('6*9', 42),
marks=pytest.mark.xfail,id='failed')])
如果不想执行多次,可以使用模糊匹配来匹配具体到具体的实例: pytest -k 163 用例 //固件进行参数化时,一定是一个实例执行完后(含前后置处理,才开始新的实例工作)
//同上固件可以打标记,配置测试ID;使用参数化装饰器等价的方法如下:
@pytest.mark.parametrize('test_input, expected',[('3+5', 8),
pytest.param('6*9', 42, marks=pytest.mark.xfail,id='failed')])
//给固件重命名后,原来的名字不可用,只能使用新的名称来显示的调用固件:@pytest.fixture(name="my_fixture_alias")
用例按条件执行固件:@pytest.mark.parametrize("condition", [True, False])
//复写操作,除了在conftest中,用例模块中,还可以在用例的参数化中进行复写(不做特殊配置时,参数名会覆盖同名固件)
@pytest.mark.parametrize('已定义固件名', ['directly-overridden-username'])
每个用例可以执行多个固件;执行顺序是显示调用优先级>自动调用;按作用域大小来,作用域越大越优先;作用域相同时,按从上到下的顺序来,不过,固件中参数是固件时调用的固件要优先执行;查找固件的顺序则按就近原则;- pytest内置的固件
1
2
3
4
5
6
7
8pytest有现成的一些固件,可以直接在测试用例中调用,或者在自定义的固件中调用;
查看现成的固件的方法:pytest -q -v --fixtures;
在程序执行时,动态的修改对象(使用固件monkeypatch);这种修改是临时性的,不改本地代码;
创建临时文件/目录,(使用tmp_path);
在测试用例中访问捕获信息(即标准输出/标准错误):可以使用固件capsys、capfd、capsysbinary
capsys捕获的是系统级别,如capsys.readouterr()结果是个系统级别的标准输出和标准错误的元组
capfd捕获的是文件/接口级别,如capfd.readouterr()结果是个接口级别的标准输出和标准错误的元组
capsysbinary用来返回非文本型的数据,capsysbinary.readouterr()返回的是字节流
特殊场景-文档测试
1
2
3
4
5
6
7
8
9
10文档测试/注释测试;pytest会自动收集所有名称匹配test*.txt规则的文件,并调用doctest执行它们;注释中的代码的执行使用的是Python的标准库doctest模块来实现的;
注释需要满足几个条件:
①文件名称是以test开头或结尾的Python文件;
②文档字符串中的注释必须是类似python交互式会话形式的注释(>>> something())
//可以通过pytest --doctest-glob='*.rst'来扩展支持的文档格式
//执行文档中的注释代码,必须要加选项pytest --doctest-modules命令行选项;也可以在pytest.ini文件中配置指定必须要文档测试[pytest]
addopts = --doctest-modules
//用pytest --doctest-continue-on-failure选项在文档测试遇到第一个失败后继续执行
//可以定制文档测试失败后的,失败输出格式pytest --doctest-modules --doctest-report none/udiff/cdiff/ndiff
通过在文档中添加''''>>> tmp = getfixture('tmpdir')'''的方式在文档中使用固件;通过doctest_namespace 固件定义的参数,可以在文档中直接使用使用pytest.skip(‘注释’),直接跳过文档测试,默认不执行文档测试
用例:(断言+异常处理+跳过+标记)
断言
- 示例除了断言还可以设置断点:使用pdb.set_trace(),只影响断点用例;
1
2
3
4
5
6
7
8
9
10
11
12
13
14#常规断言
def test_
assert True //如果判断条件的结果为True则断言成功;
#指定预期异常
def test_mytest():
with pytest.raises(SystemExit) as excinfo:
f() //若用例没抛出指定异常则断言失败,且必须是上下文中的最后一行,因为raises之后的不会被执行
assert '456' in str(excinfo.value) //excinfo是上下文的实例,且在上下文环境中
#可以给异常加标记
@pytest.mark.xfail(raises=IndexError) //可以给异常加标记,出现IndexError,则标记用例失败类型为xfail,这种用法一般用来标记未修复的bug
def test_f():
f()
异常的结果
- 不同数据类型的断言,不同地方都会标记出来;
- 可以给断言失败,添加说明,可以用运算重载符,出现系统提示的字符串异常时,重载为我们想要的内容;第二种是利用钩子pytest_assertrepr_compare,来自定义异常;
- 断言的异常信息,会存储在本地,如需禁止可以在conftest.py文件中加入这些配置实现:
1
2import sys
sys.dont_write_bytecode = True - 关闭断言的失败提示功能:有两种方式(基本不会关闭)
- 在需要关闭提示的模块的docstring中添加PYTEST_DONT_REWRITE字符串;
- 执行pytest时,添加–assert=plain选项
异常处理
1 | warning告警,可以手动通过warnings.warn方法来触发特定的告警; |
标记
使用标记来给用例打上不同的标记,从而在结果收集时,实现对用例的区分;
- 查看用例标记:
使用 pytest -r来定制显示不同标记的用例;支持的标记有 (f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed, (p)assed, (P)assed with output, (a)ll except passed(p/P), or (A)ll
- @pytest.mark.xfail()用来标记预期是失败的用例,如果满足预期的失败,则结果为xfail,如果是成功,则结果一般是xpass,可能是FAILED,详情如下
标记可以和参数化装饰器结合,他提供了一个方法 pytest.param,可以给每个参数对应的实例打一个标记,并给一个测试id
1
2
3
4
5
6
7
8
9@pytest.mark.parametrize(
('n', 'expected'),
[(2, 1),
pytest.param(2, 1, marks=pytest.mark.xfail(), id='XPASS'),
pytest.param(0, 1, marks=pytest.mark.xfail(raises=ZeroDivisionError), id='XFAIL'),
pytest.param(1, 2, marks=pytest.mark.skip(reason='无效的参数,跳过执行')),
pytest.param(1, 2, marks=pytest.mark.skipif(sys.version_info <= (3, 8), reason='请使用3.8及以上版本的python。'))])
def test_params(n, expected):
assert 2 / n == expected
跳过
跳过用例的方法总结如下:其中@pytest.mark.skipif //如果一个用例有多个条件跳过装饰器,满足任意一个即跳过; @pytest.importorskip装饰器是会先进行import模块操作,同时会判断模块的版本;如果版本不对,或import失败则跳过;
参数化
固件的参数化
1 | 使用@pytest.fixture(params=[('3+5', 8),pytest.param(('6*9', 42), |
测试用例参数化
参数化是指对用例的参数传递不同的值,来执行多次,从而覆盖用例不同的场景(不同值)
- 对象
- 可以对测试用例方法进行参数化
- 可以对测试类进行参数化
- 可以对模块进行参数化:
pytestmark = pytest.mark.parametrize('test_input, expected', [(1, 2), (3, 4)])后续的用例直接调用参数即可
- 方法:@pytest.mark.parametrize(argnames, argvalues, indirect=False, ids=None, scope=None)
- argnames:
参数名必须是用例引入的参数的子集(fun(arg1,arg2),必须是这两个参数的子集);且引入的参数必须是没有默认值的;结构可以是用逗号分隔的字符串,或者一个列表/元组; 参数名默认会覆盖同名的固件;
- argvalues:
实际返回的是一个命名元祖,ParameterSet(values=(1, 2), marks=[], id=None)];可以不带标记和测试id,也可以带;结构上必须是一个可迭代对象可以是[(1, 2), [2, 3], set([3, 4])]/方法返回的可迭代对象; 如果argvalue要带标记和测试ID,可以这样实现:pytest.param(2, 1, marks=pytest.mark.xfail(), id='XPASS')param方法实际是对命名元祖的封装;
- indirect参数:
有默认值,为TRUE表示参数名不覆盖同名固件,且参数值要传递给固件,最后固件的返回才是实参@pytest.mark.parametrize('min, max', [(1, 2), (3, 4)], indirect=True/['max']) //['max']仅指定这个参数名不覆盖固件
- ids:
ids是每个测试的实例的测试ID,报告可以记录,默认值是每一个实例的argvalue_拼接,如上是1_2;手动定义时ids值长度必须和argvalue长度保持一致;默认所有的id都进行了ascii编码,可以在pytest.ini中禁用ascii编码(disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True) 可以构造函数方法来使用每个的argvalue参数来生成ids,类似ids=fun()
- scope:
scope定义的是argnames的作用域,并通过argvalues的实例划分测试用例,影响到测试用例的收集顺序(不同用例,相同参数时,会根据值的大小来划分收集顺序;如果参数是固件时,且不覆盖,则参数的作用域,就是固件的作用域,按这个顺序收集测试结果,默认是function级别)
- 特殊情况:一个用例上可以有多个参数化标记,执行顺序是从近到远,组合提供值
1
2
3
4
5如果参数化没传值时,实例的结果将会被设置为SKIPPED;
这种情况可以在pytest.ini中设置empty_parameter_set_mark选项来改变这种行为,可以的值如下:
skip:默认值
xfail:执行跳过直接将示例标记为XFAIL,等价于xfail(run=False)
fail_at_collect:上报一个CollectError异常;
@pytest.mark.parametrize(‘test_input’, [1, 2, 3])
@pytest.mark.parametrize(‘test_output, expected’, [(1, 2), (3, 4)])
结果:1_2_1,1_2_2,1_2_3,3_4_1,…
- argnames:
定制报告
为测试报告中的测试用例添加额外的信息,使用record_property fixture
1 | def test_record_property(record_property): |
其他的对测试报告的定制操作,见上方选项合集
性能-缓存
是什么:
pytest执行时默认会在根目录生成一个.pytest_cache文件夹,用来记录执行的情况,主要记录执行的所有失败(lastfailed)、执行的所有实例(nodeids)、最近一次失败(stepwise)、缓存配置数据();缓存是通过pytest自带的cacheprovider插件来实现;
作用:
可以通过选项来依赖缓存的结果和配置数据来定制执行的顺序、和跳过某些用例、使用缓存配置数据,从而提高性能;
怎么样:
生成缓存文件-控制执行顺序(范围);
- 缓存文件:默认位置是根目录生成一个.pytest_cache文件夹;可以通过pytest.ini中配置cache_dir = .pytest-cache来自定义缓存文件夹路径,可配置相对和绝对路径;
- 缓存中主要起作用的是lastfailed(所有失败实例)/nodeids(所有实例)/stepwise(最近失败实例)
│ └───.pytest-cache
│ │ .gitignore
│ │ CACHEDIR.TAG
│ │ README.md
│ │
│ └───v
│ └───cache
│ lastfailed
│ nodeids
│ stepwise
- 控制执行顺序(范围):通过一些选项来利用上面的文件控制实例的执行顺序,并且过滤一些实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26查看缓存:pytest src/chapter-12/ -q --cache-show 'lastfailed'/'nodeids'/'stepwise'
pytest --lf --collect-only src/chapter-12/ //仅执行已经收集到的失败实例
pytest --collect-only -s --ff src/chapter-12/ //先执行上次收集到的失败实例,在执行其他
pytest --collect-only -s --nf src/chapter-12/ //先执行新添加的-继而执行修改的(按最晚修改最先执行)-最后执行其他实例;
pytest --cache-clear -q -s src/chapter-12/test_pass.py //先删除缓存文件夹(pytest-cache),在创建缓存文件夹,保存最新的缓存数据
//以下都用于之前没有采集到失败实例的场景
pytest -q -s --ff --lfnf none src/chapter-12/test_pass.py //会忽略失败
pytest -q -s --ff --lfnf all src/chapter-12/test_pass.py //遇到失败就结束了,只保留之前的成功的
#缓存配置数据
@pytest.fixture
def mydata(request):
val = request.config.cache.get("example/value", None) //读取缓存配置
if val is None:
expensive_computation()
val = 42
#设置缓存配置。没有文件时,会先创建在设置,已存在会覆盖旧的
request.config.cache.set("example/value", val)
return val
stepwise文件用来记录最近一次的失败;可以指定从这个失败开始执行,用此选项调度一般遇到失败pytest就会结束执行如下:
pytest --cache-clear --sw -q src/chapter-12/test_sample.py
//--cache-clear见上方删除旧缓存,--sw是(stepwise缩写),此选项执行时,遇到失败就会结束整个pytest,并将这个失败记录到stepwise文件;如果不指定--sw选项,这个文件不记录数据;下次在也--sw选项执行时,会优先执行stepwise文件中记录的实例,当遇到下一个失败时才结束,并且会使用新的失败来覆盖这个文件;
pytest --sw --stepwise-skip -q src/chapter-12/test_sample.py //跳过这个失败,继续执行后面的,遇到后续的失败在结束;