注:内容不全和官方文档相同,只是按照官方文档顺序随心记录,与诸位做参考而已
开始
安装pytest
使用命令 pip install -U pytest
即可安装pytest 使用 pytest --version
可以确认pytest安装是否成功以及安装的版本
整体测试环境如下:
(real) aaron@localhost:/raven/project/PycharmProjects/first> python -V
Python 3.9.4
(real) aaron@localhost:/raven/project/PycharmProjects/first> pip -V
pip 21.1.3 from /home/aaron/.virtualenvs/real/lib/python3.9/site-packages/pip (python 3.9)
(real) aaron@localhost:/raven/project/PycharmProjects/first> pytest --version
pytest 6.2.5
创建第一个测试
- 新建一个叫做 test_sample.py 文件中包含一个方法和测试
"""
pytest 默认会查找当前目录以及其子目录中所有的test_*.py或者*_test.py格式的文件
并且运行其中test_*格式的方法
不符合格式要求的文件以及方法会被略过
"""
# 测试的方法,比如这里我们写了一些逻辑,获取各种信息并处理返回
# 这里只是简单逻辑,作为示例使用
def func(x):
result = f"拿到{x},做一些运算....."
return result
# pytest检测到这里就会执行这个函数
def test_func():
# 正常业务处理
x = "some value"
value = func(x)
# 打印当前
print("=" * 20)
print(value)
print("=" * 20)
# 断言。pytest直接使用断言判断用例失败
assert x in value
- 运行测试文件。pytest运行还是十分简单的,只要terminal中进入目录,直接输入pytest即可。
(real) aaron@localhost:/raven/project/PycharmProjects/first> pytest
=============================================== test session starts ===============================================
platform linux -- Python 3.9.4, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /raven/project/PycharmProjects/first
collected 1 item
test_sample.py . [100%]
================================================ 1 passed in 0.10s ================================================
(real) aaron@localhost:/raven/project/PycharmProjects/first> pytest -s
=============================================== test session starts ===============================================
platform linux -- Python 3.9.4, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /raven/project/PycharmProjects/first
collected 1 item
test_sample.py ====================
拿到some value,做一些运算.....
====================
.
================================================ 1 passed in 0.10s ================================================
(real) aaron@localhost:/raven/project/PycharmProjects/first>
注意上下两次执行结果的不同,pytest默认不打印print的内容,如果需要打印,需要添加-s参数
下面是正常执行的过程内容:
# 输入pytest执行目录下的所有符合规则的文件以及文件中的方法,这里只是说符合规则是因为匹配模式是可以修改的,具体如何修改会在之后有介绍
(real) aaron@localhost:/raven/project/PycharmProjects/first> pytest
# pytest开始执行用例
=============================================== test session starts ===============================================
# 打印基础信息
platform linux -- Python 3.9.4, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /raven/project/PycharmProjects/first
# pytest玩命采集到1个符合要求的用例
collected 1 item
# 赶紧执行用例,用例在test_sample.py中,后面跟着几个点就是有几个用例执行成功
# 最后的100%是显示的执行完成的用例所占的百分比
test_sample.py . [100%]
# 用例执行完成,展示执行结果,这里显示为1个通过,并且在0.10s内执行完成
================================================ 1 passed in 0.10s ================================================
- 既然pytest使用断言判断测试用例是否执行成功,那么我们就可以自然得出一个推断,它应该对错误也有捕获。
修改测试用例新增test_fun2 test_fun3和test_fun4函数,部分内容为下面的内容:
def func(x):
result = f"拿到{x},做一些运算....."
return result
def test_func():
......
assert x in value
def test_fun2():
x = "2a"
x = int(x)
assert x == 2
def test_func3():
assert 3 == 3
def test_func4():
pass
执行结果为:
(real) aaron@localhost:/raven/project/PycharmProjects/first> pytest
====================================================================== test session starts =======================================================================
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /raven/project/PycharmProjects/first
collected 4 items
test_sample.py .F.. [100%]
============================================================================ FAILURES ============================================================================
___________________________________________________________________________ test_fun2 ____________________________________________________________________________
def test_fun2():
x = "2a"
> x = int(x)
E ValueError: invalid literal for int() with base 10: '2a'
test_sample.py:31: ValueError
==================================================================== short test summary info =====================================================================
FAILED test_sample.py::test_fun2 - ValueError: invalid literal for int() with base 10: '2a'
================================================================== 1 failed, 3 passed in 0.02s ===================================================================
接下来来一起分析一下报错。
# 执行pytest
(real) aaron@localhost:/raven/project/PycharmProjects/first> pytest
# 开始执行测试用例
====================================================================== test session starts =======================================================================
# 展示基本信息
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /raven/project/PycharmProjects/first
# 收集到了4个测试用例
collected 4 items
# 执行test_sample.py文件,第二个失败了,所以显示了F
# 所有4个用例都执行了,所以总体执行进度为100%
# 可以看到中间的用例执行失败,发生异常并不会影响后续用例的执行
# 同时判断条件比较宽泛,只要没有引起异常就判断为正常通过,不会管测试用例执行逻辑
test_sample.py .F.. [100%]
# 这次因为有执行错误的用例,所以会进行单独显示
============================================================================ FAILURES ============================================================================
# 执行错误用例的用例名称
___________________________________________________________________________ test_fun2 ____________________________________________________________________________
def test_fun2():
x = "2a"
# 用例执行错误的位置
> x = int(x)
# 错误的原因
E ValueError: invalid literal for int() with base 10: '2a'
# 对错误的其他信息展示
test_sample.py:31: ValueError
==================================================================== short test summary info =====================================================================
FAILED test_sample.py::test_fun2 - ValueError: invalid literal for int() with base 10: '2a'
================================================================== 1 failed, 3 passed in 0.02s ===================================================================
断言一个确定的异常
如果一个异常是我们已经确定的了,可以指定异常,从而能跳过异常判断
# 导入pytest库
import pytest
def fun(x):
# 返回字典x的键a所对应的值
return x["a"]
def test_func():
# x定义为空字典
x = {}
# 通过使用pytest.raises包裹指定的异常,而对指定的异常不判定为失败
with pytest.raises(KeyError):
# 这里发生了KeyError的错误,但是因为使用pytest.raises包裹,并不判断为失败
fun(x)
def test_func2():
with pytest.raises(KeyError):
# 这里会先发生异常,但是包裹的是KeyError而发生的是ValueError,所以会判定用例失败
x = int("a")
fun(x)
def test_func3():
# 多个异常可以使用元祖类型进行传入,这里忽略两种异常,所以这个用例执行会成功
with pytest.raises((KeyError, ValueError)):
x = int("a")
fun(x)
def test_func4():
# 这里这个用例主要是要看看正常assert产生错误的时候如果有变量,pytest会把变量也进行打印,方便判断
x = {"a": 2}
assert fun(x) == 3
整体的输出如下,请结合代码中的解释自行分析输出结果
(real) aaron@localhost:/raven/project/PycharmProjects/first> pytest
\====================================================================== test session starts =======================================================================
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /raven/project/PycharmProjects/first
collected 4 items
test_sample.py .F.F [100%]
============================================================================ FAILURES ============================================================================
___________________________________________________________________________ test_func2 ___________________________________________________________________________
def test_func2():
with pytest.raises(KeyError):
> x = int("a")
E ValueError: invalid literal for int() with base 10: 'a'
test_sample.py:16: ValueError
___________________________________________________________________________ test_func4 ___________________________________________________________________________
def test_func4():
x = {"a": 2}
> assert fun(x) == 3
E AssertionError: assert 2 == 3
E + where 2 = fun({'a': 2})
test_sample.py:28: AssertionError
==================================================================== short test summary info =====================================================================
FAILED test_sample.py::test_func2 - ValueError: invalid literal for int() with base 10: 'a'
FAILED test_sample.py::test_func4 - AssertionError: assert 2 == 3
================================================================== 2 failed, 2 passed in 0.03s ===================================================================
(real) aaron@localhost:/raven/project/PycharmProjects/first>
使用class编组测试
如果有一组测试用例可以进行分组,那么可以将测试用例放进class中,比如下面这样
# 编组的class 命名规则必须符合Test*的格式,注意,与函数、方法和文件不同,类命名的时候不强制要求带有下划线
# 同时,类中不能存在__init__方法的存在,否则也会被忽略不执行
class TestClass:
# 与函数相同,如果想被测试,也需要test_开头
def test_1(self):
assert 1 == 1
def test_2(self):
assert 2 == 2
接下来我所有的用例执行都会使用pycharm进行手动执行,事例的格式有些不同,但是整体思路是相同的。
安装pytest后,pycharm会在每个可以执行的用例左边有一个绿色三角的运行标志。
使用那个标志运行即可,pycharm运行时添加了一些参数,我这里先说一下,之后我就只截取显示部分,其余部分诸君请自行尝试。
这里我点的是TestClass左边的绿色执行按钮。
# "/home/aaron/.virtualenvs/real/bin/python"这个是python执行文件
# "/raven/soft/pycharm/plugins/python/helpers/pycharm/_jb_pytest_runner.py" pycharm 执行pytest的插件脚本
# "--target test_sample.py::TestClass" 执行目标,使用pytest也可以这样执行 比如pytest test_sample.py::TestClass 就只会执行TestClass测试用例,其他的测试用例都会被忽略
/home/aaron/.virtualenvs/real/bin/python /raven/soft/pycharm/plugins/python/helpers/pycharm/_jb_pytest_runner.py --target test_sample.py::TestClass
# 测试开始执行时间
Testing started at 12:37 AM ...
# 警告信息,pycharm插件问题,可以忽略
/raven/soft/pycharm/plugins/python/helpers/pycharm/_jb_pytest_runner.py:6: DeprecationWarning: The distutils package is deprecated and slated for removal in Python 3.12. Use setuptools or check PEP 632 for potential alternatives
from distutils import version
# 配置pytest参数 --no-header 不展示头信息,也就是目录版本等信息 --no-summary 没有总结信息 -q 最简短模式输出,使用--quit效果相同
Launching pytest with arguments test_sample.py::TestClass --no-header --no-summary -q in /raven/project/PycharmProjects/first
# 开始真正的测试输出
============================= test session starts ==============================
# 采集到了两个测试用例
collecting ... collected 2 items
# 执行了哪个用例,执行结果如何,执行完成本用例后整体用例执行了多少
test_sample.py::TestClass::test_1 PASSED [ 50%]
test_sample.py::TestClass::test_2 PASSED [100%]
# 简短总结信息
============================== 2 passed in 0.01s ===============================
Process finished with exit code 0
现在我们将测试用例进行修改,再执行一下试试
class TestClass:
def test_1(self):
assert 1 == 1
def test_2(self):
# 修改这里的判断条件,必定会执行失败
assert 3 == 2
输出如下,请诸君自行分析输出的信息
/home/aaron/.virtualenvs/real/bin/python /raven/soft/pycharm/plugins/python/helpers/pycharm/_jb_pytest_runner.py --target test_sample.py::TestClass
Testing started at 1:02 AM ...
/raven/soft/pycharm/plugins/python/helpers/pycharm/_jb_pytest_runner.py:6: DeprecationWarning: The distutils package is deprecated and slated for removal in Python 3.12. Use setuptools or check PEP 632 for potential alternatives
from distutils import version
Launching pytest with arguments test_sample.py::TestClass --no-header --no-summary -q in /raven/project/PycharmProjects/first
============================= test session starts ==============================
collecting ... collected 2 items
test_sample.py::TestClass::test_1
test_sample.py::TestClass::test_2 PASSED [ 50%]FAILED [100%]
test_sample.py:4 (TestClass.test_2)
3 != 2
Expected :2
Actual :3
<Click to see difference>
self = <test_sample.TestClass object at 0x7f5fbded9bd0>
def test_2(self):
# 修改这里的判断条件,必定会执行失败
> assert 3 == 2
E assert 3 == 2
test_sample.py:7: AssertionError
========================= 1 failed, 1 passed in 0.03s ==========================
Process finished with exit code 1
需要注意的一点是,虽然将几个方法进行分组了,但是几个方法之间并不能共享修改的变量,这里使用官方的一个例子说明
class TestClass:
value = 0
def test_func1(self):
self.value = self.value + 1
assert self.value == 1
def test_func2(self):
assert self.value == 1
输出结果为:
============================= test session starts ==============================
collecting ... collected 2 items
test_sample.py::TestClass::test_func1
test_sample.py::TestClass::test_func2 PASSED [ 50%]FAILED [100%]
test_sample.py:7 (TestClass.test_func2)
0 != 1
Expected :1
Actual :0
<Click to see difference>
self = <test_sample.TestClass object at 0x7effad1420a0>
def test_func2(self):
> assert self.value == 1
E assert 0 == 1
test_sample.py:9: AssertionError
========================= 1 failed, 1 passed in 0.03s ==========================
可以看到虽然test_sample.py::TestClass::test_func1先执行了,但是test_sample.py::TestClass::test_func2依旧失败了,self.value的值依旧是0 这是为什么呢?
给上面的代码增加一些东西
import json
import time
# 构造一个记录类,记录测试用例执行的时间以及在内存中的地址
class RecordClass:
# 记录的列表
record_list = []
# 这里i是实例化后的
def __init__(self, i=0):
# 当前时间
t = time.time()
# 将时间和内存中的地址添加到list
self.record_list.append((t, i))
# 将列表存入文件,方便查看
with open("record_list.txt", "a") as f:
f.write(json.dumps(self.record_list))
f.write("\n")
class TestClass:
value = 0
def test_func1(self):
# 记录自身的内存地址
RecordClass(id(self))
self.value = self.value + 1
assert self.value == 1
def test_func2(self):
RecordClass(id(self))
assert self.value == 1
继续执行TestClass 然后查看record_list.txt内容如下:
# test_sample.py::TestClass::test_func1的类内存地址为139887927254032
[[1634199757.6280792, 139887927254032]]
# test_sample.py::TestClass::test_func2的类内存地址为139887927253600
[[1634199757.6280792, 139887927254032], [1634199757.6308126, 139887927253600]]
根据内存地址的不同,可以看到两个case执行了两个不同的类,也就是说是两个单独的事例,也就是func1修改的只是func1实例的self.value没有影响到func2的实例,所以导致func2的self.value还是默认的0,于是失败了。
至于如果在类间传递变量也是有方法的,还是需要使用pytest提供的方法。对于上面失败的原因,本人盲猜是因为pytest内部使用了多线程,每个测试用例一个线程去跑,当然具体原因还是有时间的时候看看源代码再了解吧。
注意:RecordClass中的record_list能够不断写入的原因是因为record_list是一个可变类型,如果TestClass的value属性也是可变类型也会不断存入数据。具体原因请各位自行查阅,这里不在赘述。
pytest内置的参数
pytest包含很多内置参数,提供给需求的开发者调用,这里使用官网的例子举例,具体的查看地址为 Builtin fixtures/function arguments
def test_tmp_dir(tmpdir):
print(tmpdir)
assert 0
其中tmpdir就是pytest提供的一个内置参数。整体函数执行的结果如下:
============================= test session starts ==============================
collecting ... collected 1 item
test_sample.py::test_tmp_dir FAILED [100%]/tmp/pytest-of-aaron/pytest-3/test_tmp_dir0
test_sample.py:0 (test_tmp_dir)
tmpdir = local('/tmp/pytest-of-aaron/pytest-3/test_tmp_dir0')
def test_tmp_dir(tmpdir):
print(tmpdir)
> assert 0
E assert 0
test_sample.py:3: AssertionError
============================== 1 failed in 0.05s ===============================
可以看到已经把临时目录打印出来了。
如果想通过命令行确认含有哪些内置属性或者手动定制属性都可以通过pytest --fixtures
进行查询