一.虚拟环境与pytest配置
首先你需要准备好pycharm
,并且安装好python
环境。(mac
基本都自带python
,3.7.8
及其以后的版本都可用)。然后按照以下步骤,配置好虚拟环境。
此时,这个虚拟环境就创建好了,我们可以看到根目录下多了一个橙红色的venv
文件夹。
然后我们再创建一个requirements.txt
文件,一个pytest.ini
文件
# requirements.txt allure-pytest==2.8.6 allure-python-commons==2.8.6 pytest==6.2.3 pytest-assume==2.2.1 pytest-cov==2.8.1 pytest-cover==3.0.0 pytest-coverage==0.0 pytest-dependency==0.5.1 pytest-forked==1.4.0 pytest-pythonpath==0.7.4 pytest-ordering==0.6 pytest-repeat==0.9.1 pytest-rerunfailures==11.0 pytest-xdist==1.30.0 python-dateutil==2.8.2 retry==0.9.2 retrying==1.3.4
pytest.ini
是pytest
的运行配置文件。pytest会在运行目录中自动找到文件,并读取配置。这里面预设了一些配置,能省去我们之后在命令行里敲配置。
[pytest] markers = p0: 优先级标 marks tests as p0 p1: 优先级标 marks tests as p1 p2: 优先级标 marks tests as p2 python_paths = . addopts = -v -s --alluredir=reports/ecs --junit-xml=reports/result.xml --import-mode=importlib python_classes = Test* python_files = test* python_functions = test* # 这里配置一下之后,在allure的界面上,可以看到格式化之后的日志 log_cli = True log_level = INFO log_format = %(asctime)s.%(msecs)03d[%(levelname)s]%(pathname)s:%(funcName)s:%(lineno)d: %(message)s log_date_format = %Y-%m-%d %H:%M:%S # 虽然pytest自己就可以设置日志相关参数,但是有个大问题,在启用pytest-xdist之后,日志无法输出到控制台 # 这是相关的issue:https://github.com/pytest-dev/pytest-xdist/issues/574 # 因此,我们不得不设置两遍: # 1. 在pytest侧设置一遍,用于格式化allure中的日志 # 2. 自己用logger.conf来设置一遍logger
最后两步,我们安装一下依赖,并检查一下, 目前根目录下应该有这些东西(图1),打开pycharm
自带的命令行工具,并运行pip3 install -r requirements.txt
(图2)
图1
接着在命令行里输入pytest --version
,确认一下pytest
是否安装完成
二.PyCharm的配置
打开preferences
,按如下设置,然后重启打开这个项目
如果你之前已经有了一些运行配置,则需要把他们先全删掉
三.第一个测试
我们创建一个test_my_heart.py
文件,编辑这些内容
import logging def my_heart_status(): return "bad" def test_my_heart(): """ 测试一下我的心脏是否在跳动 """ logging.info("即将检查我的心脏") assert my_heart_status() == "healthy"
在命令行中输入pytest
得到这些输出:
是不是神奇,又很简单?
四.pytest
怎么知道哪些函数是测试?
这里,pytest
在目录下递归寻找测试函数,并运行。查找的规则如下:
-
寻找以
test
开头或结尾的py
文件 -
在第一步中找到的文件中寻找以
Test
开头的类,并且没有__init__
方法(可省略) -
在第一步中找到的文件中寻找以
test_
开头的函数,或者在第二步中找到的类中寻找以test
开头的方法。
五.assert是什么?
assert
是断言,简而言之,assert
后面跟一个表达式,并且这个表达式返回bool
值。如果bool
值是True
,则测试通过;如果是False
,则测试失败。
注意!在pytest
中写测试的时候,一定要使用assert
去做测试结果的断言,而不要主动抛出错误(比如raise error
)。pytest mock
了assert
的实现,因此可以捕捉到用例的失败,以及失败时的上下文。注意看下图中pytest
主动输出的上下文,如果主动raise error
,则不会有这么清晰的上下文输出。
六.优雅使用assert
import logging def my_heart_status(): return "bad" def test_my_heart(): """ 测试一下我的心脏是否在跳动 """ logging.info("即将检查我的心脏") assert my_heart_status() == "healthy", f"检查我的心脏失败,实际结果为{my_heart_status()}"
报错信息对比
七.生成测试报告
可以注意到,刚刚运行完之后,在根目录多了一个reports
文件夹。这个文件夹存放了测试结果,但是这个测试结果不是人类可读的,因此我们需要生成一个人类可读的测试报告。虽然命令行的输出也能看,但是生成的测试报告实在太方便了,因此我强烈建议用网页版的测试报告来查看测试结果。
我们首先安装一下allure:brew install allure
。然后运行allure generate reports/ecs -o reports/html --clean
。我们就生成了人类可读的html
测试报告。然后打开这个html
,并把语言转成中文。
类别、测试套、功能、包,这四个title
都可以查看到所有的测试用例执行情况。但是我最推荐功能来查看,因为这个title
展示起来最清晰,需要点击的次数最少。下面是在面对几十个测试用例下,几个tab
页的区别。
八.查看失败的原因
回到我们自己的例子上,我们点击功能,点击我们刚刚失败的case
,我们可以看到是哪个case
失败了。更进一步,我们点开几个框框,可以看到具体的日志和测试详情。
九.打标签
使用pytest
的mark
能力,我们可以给case
打tag
,然后按tag
来运行不同的case
。比如最基础的,区分P0
与P1 case
。我们来修改一下之前的代码。
import logging import pytest def my_heart_status(): return "stopped" def my_heartbeat(): return -1 @pytest.mark.p0 def test_my_heart_status(): """ 测试一下我的心脏是否在跳动 """ logging.info("即将检查我的心脏") assert my_heart_status() == "running" @pytest.mark.p1 def test_my_heartbeat(): """ 测试一下我的心脏跳动是否正常 """ logging.info("即将检查我的心脏") assert 50 < my_heartbeat() < 180
然后我们运行pytest -m p0
,会得到如图3结果。
可以看到pytest
把两个用例都识别出来了,但是只跑了标记了p0
的用例,而标记了p1
的用例没有跑
图3
此外,所有的mark
需要提前声明。在这个case
中,我们在pytest.ini
文件中已经提前声明好了一些mark
(图4),如果没有提前声明,那么pytest
会有报警(图5)。
图4
图5
十.暂时不运行
如果某个用例暂时有问题,或者用例写好了,但是功能没写好,我们可以标记先跳过这个用例。
更进一步,我们可以有条件得跳过某些case
,比如我们先看看当前的环境,是否在ICU
,如果在的话,就不管心跳了。
十一.同一个用例不同的参数
比如现在有一个查询的测试接口,我们要测试不同查询条件下接口是否符合预期。从最简单的开始:不同的page size。
可以看到,虽然我们只写了一个用例,但是利用pytest.mark.parametrize
,我们实现了类似表驱动测试的效果。
下面使用了pytest -k
参数。
我们也可以在一个pytest.mark.parametrize
内设置多个参数。
我们还可以让多个参数排列组合,只需要添加多个pytest.mark.parametrize
即可。
十二.Fixture,前置依赖
fixture
(中文名叫做夹具)是pytest
最重要的一块功能,pytest
可以通过fixture
来指定前置依赖,并且pytest
将解析依赖顺序,然后按照顺序一个一个函数执行。如果前置依赖执行失败了,那么后续的操作就自动不会执行。以上是fixture
的特点。而我们使用fixture
,主要看中其三个功能:
-
声明前置依赖(可以在测试报告里看到,而不用看代码了)。
-
缓存同一作用域下的前置依赖,并让多个用例共用一个前置依赖(通过复用资源,提高运行速度,减少资源占用)。
-
在case运行完之后清理资源。(方便~)
先来一个简单的例子,我们把之前主动调用的my_heart_status
与my_heartbeat
改成前置依赖。代码如下:
import logging import pytest @pytest.fixture() def my_heart_status(): return "running" @pytest.fixture() def my_heartbeat(): return 80 @pytest.mark.p0 def test_my_heart_status(my_heart_status): """ 测试一下我的心脏是否在跳动 """ logging.info("即将检查我的心脏") assert my_heart_status == "running" @pytest.mark.p1 def test_my_heartbeat(my_heartbeat): """ 测试一下我的心脏跳动是否正常 """ logging.info("即将检查我的心脏") assert 50 < my_heartbeat < 180
解释一下,上述代码,通过给函数加上@pytest.fixture
这个装饰器,pytest
可以收集到所有的fixture
。然后pytest
会分析每个测试用例的入参,按命名找到对应的fixture
,执行fixture
,并把fixture
的结果传给测试用例。
同时,在我们使用了fixture
之后,在测试报告中也能看到测试用例的前置依赖。如果前置依赖失败而导致用例失败,也能在测试中清晰看到。
十三.conftest
刚刚我们把测试用例与fixture
写在同一个py
文件内,那么这些fixture
只能在这个py
文件内使用。如果要让fixture
能被多个py
文件使用,则需要把fixture
写到conftest.py
文件中。pytest
会递归寻找目录下名为conftest.py
的文件,并使这些fixture
在其子目录都可用。
举个例子:共有三层目录,两个conftest.py
文件,三个test.py
文件。
- conftest.py # 有fixtureA - test_x.py - TestOtherDir # 这是一个文件夹 - conftest.py. # 有fixtureB - test_y.py - TestZDir # 这是一个文件夹 - test_z.py
上面的例子中,fixtureA
可以被三个test.py
文件使用。fixtureB
只能被test_y.py
、test_z.py
使用。
让我们新建一个conftest.py
文件,并把两个fixture
都放进入。
十四.作用域 & 缓存与共用fixture
fixture
的一个大用途在于:在同一作用域下多个case
共享同一个前置依赖。我们将上述的例子改一下,假设通过一个方法就可以获得heart
的所有信息。
# 在 conftest.py中 import logging import pytest class Heart: def __init__(self, status, beat): self.status = status self.beat = beat @pytest.fixture() def my_heart(): logging.info("获取heart信息") return Heart("running", 80) # 在 test_heart.py中 @pytest.mark.p0 def test_my_heart_status(my_heart): """ 测试一下我的心脏是否在跳动 """ logging.info("检查心脏状态") assert my_heart.status == "running" @pytest.mark.p1 def test_my_heartbeat(my_heart): """ 测试一下我的心脏跳动是否正常 """ logging.info("检查心跳") assert 50 < my_heart.beat < 180
my_heart
函数调用了两次。这是因为每个fixture
默认的作用域是function
级别的,即每个测试用例都会重新执行一遍这个fixture
。
作用域一共有四种:
-
function
:每个测试用例都运行一次该fixture
(默认) -
class
:class
内所有方法只运行一次该fixture
(一个类内可以有多个测试方法) -
module
:一个.py
文件只执行一次该fixture
-
session
:每次调用pytest
命令下,只执行一次(跨多个py
文件,多个文件夹)
我们将其作用域改为module
,然后看看执行结果。可以看到这个fixture
只会执行一次。
如果我们将两个测试用例放在两个py
文件下,则又会执行两次。
如果我们将两个测试用例放在两个py
文件下,但是作用域设置成session
,则只会执行一次。
十五.嵌套
除了case
可以通过fixture
设置前置依赖,fixture
本身也可以设置前置依赖~
# 在 conftest.py中 import logging import pytest class Heart: def __init__(self, status, beat): self.status = status self.beat = beat @pytest.fixture() def prepare(): return "abc" @pytest.fixture() def my_heart(prepare): logging.info("获取heart信息" + prepare) return Heart("running", 80)
十六.可传参Fixture
我们可以通过把函数调用转为fixture
依赖,来获得更加直观的测试报告与前置依赖缓存。但是在函数调用时可以传递参数,那我们怎么给fixture
传递参数呢?
接着上面的例子,我们需要检查不同年龄、不同性别的心脏。
在fixture
中,需要将第一个参数设置为request
,然后可以通过request.param
拿到测试用例传来的所有参数。
在测试用例中,用法基本pytest.mark.parametrize
相同,只需要添加indirect=True
即可。
# 在 conftest.py 中 import pytest class Heart: def __init__(self, status, sex, beat): self.status = status self.sex = sex self.beat = beat @pytest.fixture(scope="module") def my_heart(request): logging.info("获取heart信息") return Heart("running", request.param["sex"], request.param["age"] * 5) # 在 test_my_heart.py 中 import logging import pytest @pytest.mark.parametrize("my_heart", [{"sex": "female", "age": 13}, {"sex": "male", "age": 15}], indirect=True) # 注意这里! def test_my_heartbeat(my_heart): """ 测试一下我的心脏跳动是否正常 """ logging.info("检查心跳") assert 50 < my_heart.beat < 180
十七.可传参Fixture的作用域
前文提到,在同一作用域下的fixture
只会被执行一次。那如果我们给fixture
传递了不同的参数参数,作用域会发生什么样的变化呢?
接着上面的例子,conftest.py
的代码不变,我们修改test_my_heart.py
的代码。
import logging import pytest user1 = {"sex": "male", "age": 15} user2 = {"sex": "female", "age": 13} @pytest.mark.parametrize("my_heart", [user1], indirect=True) def test_my_heart_status(my_heart): """ 测试一下我的心脏是否在跳动 """ logging.info("检查心脏状态") assert my_heart.status == "running" @pytest.mark.parametrize("my_heart", [user2], indirect=True) def test_my_heartbeat(my_heart): """ 测试一下我的心脏跳动是否正常 """ logging.info("检查心跳") assert 50 < my_heart.beat < 180
这样运行一遍,我们会发现fixture
会执行两次。而如果我们把user2
改成user1
,则只会运行一遍。因此,我们可以得出结论:对于可传参的fixture
,参数+fixture
共同构成唯一一个前置依赖。
十八.资源清理
比如我们想观测Heart
,得先连接一个监听器到Heart
上,在我们测试结束之后,再把监听器给卸载掉。首推的做法是使用yield
,这个写法更加简单清晰。
@pytest.fixture(scope="module") def my_heart(request): logging.info("连接监听器") logging.info("获取heart信息") yield Heart("running", request.param["sex"], request.param["age"] * 5) logging.info("卸载监听器")
其次推荐的是使用fixture
的request
参数,request.addfinalizer()
。我们来修改一下之前的my_heart fixture
。
@pytest.fixture(scope="module") def my_heart(request): logging.info("连接监听器") def teardown(): # 这个函数的名字是随意的,也可以叫别的 logging.info("卸载监听器") request.addfinalizer(teardown) # 这一行,注册一个fixture生命周期结束后运行的函数 logging.info("获取heart信息") return Heart("running", request.param["sex"], request.param["age"] * 5)
有两个点需要注意:
第一是两种方式存在细微的逻辑差别,当下图1中红色框框内的代码段报错时(raise error
),使用addfinalizer
会执行teardown
的逻辑(前提是addfinalizer
先于报错代码段运行),而使用yield
则不会。
第二是要注意,写teardown
的时候,要考虑红色框框内失败的情况。举一个例子,有一个创建资源的fixture
,并且内置删除资源的teardown
逻辑。当创建资源失败时,teardown
去删除资源,可能会报错NotFound
,此时不应该raise error
。
十九.执行顺序与清理顺序
首先需要注意的是,由于并发跑测试、每次跑的测试集不同,pytest
跑用例的顺序总是没有规律的。因此case
与case
之间最好不要有任何的顺序依赖关系。如果case
之间依赖相同的fixture
,最好在每次执行完之后,把这个fixture
还原到最开始的状态。
其次,fixture
自身的执行顺序和清理顺序是有迹可循的,其按照以下顺序以此执行。
首先执行autouse=True
的fixture
,其次执行某个case
依赖的fixture
,如果一个case
依赖多个fixture
,则按顺序,从左到右依次执行。其次执行fixture
依赖的fixture
,如果一个fixture
依赖多个fixture
,则按顺序,从左到右依次执行,并按DFS
(深度优先)递归解析依赖。而对于清理操作,则会按上述顺序反着来,即最先被执行的fixture
,最后被清理。
我们新起一个py
文件来看看,左侧展示了DFS
的规则,右侧展示autouse
的影响。对于这个case
,大家可以自行修改代码并执行,体会一下依赖关系。
# test_order.py import pytest @pytest.fixture() def a(): print("准备AAA") yield "a" print("清理AAA") @pytest.fixture() def b(): print("准备BBB") yield "b" print("清理BBB") @pytest.fixture() def c(a, b): print("准备CCC") yield "c" print("清理CCC") def test_order_1(c): pass 准备AAA 准备BBB 准备CCC PASSED 清理CCC 清理BBB 清理AAA # test_order.py import pytest @pytest.fixture() def a(): print("准备AAA") yield "a" print("清理AAA") @pytest.fixture(autouse=True) def b(): print("准备BBB") yield "b" print("清理BBB") @pytest.fixture() def c(a, b): print("准备CCC") yield "c" print("清理CCC") def test_order_1(c): pass 准备BBB 准备AAA 准备CCC PASSED 清理CCC 清理AAA 清理BBB
二十.如何只跑一个测试?
第一种是直接点击pycharm
的这个箭头,就可以。
第二种是直接在命令行中调用pytest -k XXX
,XXX
是你的测试函数或者测试类。如果你的测试函数名与其他地方的测试函数名重合,则可以加上路径,比如pytest FFF -k XXX
。其中FFF
是你所要运行的用例的路径,精确到文件夹或者文件都可以。
第三种是不使用-k
,直接pytest XXX
。
二十一.某个fixture找不到
python
的函数很容易被同名的变量、或者其他函数覆盖。有一个很诡异的例子。
# 在 conftest.py 中 from a_fixture import * from a import * # 在 a_fixture.py 中 @pytest.fixture def aaa(): pass # 在 a.py 中 aaa = None
在上述例子中,运行pytest --fixtures
是找不到aaa
这个fixture
的。但是如果把两个import
的顺序替换一下,就又能看到了。
二十二.只运行上次失败的case
在运行完之后,运行结果会保存在reports
目录下。我们可以运行pytest --lf
来只运行上次失败的case
。
二十三.allure的报告很混乱
因为每次运行的结果都会保存在reports
目录下,所以reports
的东西会越来越多,让allure
生成的报告也越来越复杂。因此过一段时间可以把reports
目录全部删掉,这样报告就清晰了。
二十四.单个case下的日志文件是按什么规则收集的
通常而言,一个case
执行了什么(包括前置依赖、自身的逻辑、资源清理的日志),都会在一个case
的日志中。
但是,如果多个case
依赖了同一个fixture
,那么这个fixture
的创建日志只会出现在其第一次被执行的case
的日志中,后续的case
就没有这部分日志了。
题外话
感兴趣的小伙伴,赠送全套Python学习资料,包含面试题、简历资料等具体看下方。
👉CSDN大礼包🎁:全网最全《Python学习资料》免费赠送🆓!(安全链接,放心点击)
一、Python所有方向的学习路线
Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照下面的知识点去找对应的学习资源,保证自己学得较为全面。
二、Python必备开发工具
工具都帮大家整理好了,安装就可直接上手!
三、最新Python学习笔记
当我学到一定基础,有自己的理解能力的时候,会去阅读一些前辈整理的书籍或者手写的笔记资料,这些笔记详细记载了他们对一些技术点的理解,这些理解是比较独到,可以学到不一样的思路。
四、Python视频合集
观看全面零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。
五、实战案例
纸上得来终觉浅,要学会跟着视频一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
六、面试宝典
简历模板
👉CSDN大礼包🎁:全网最全《Python学习资料》免费赠送🆓!(安全链接,放心点击)
若有侵权,请联系删除