news 2026/4/18 14:03:39

写给不常写Python的人的pytest全教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
写给不常写Python的人的pytest全教程

先利其器

虚拟环境与pytest配置

首先你需要准备好pycharm,并且安装好python环境。(mac基本都自带python,3.7.8及其以后的版本都可用)。然后按照以下步骤,配置好虚拟环境。

本文档是教程,不会涉及到具体的业务逻辑与代码。但是可能部分知识建议你配合本地调试来理解。因此建议建立一个学习项目,比如我的学习项目叫MyXXX(比如MyPython)。在开始下面的内容前,您只需要建好一个文件夹,然后用pycharm打开它就好了。
本文不涉及python3的语法(除非是一些高级特性对使用pytest有帮助的),不过python语法很简单,照着写也能写,如果想系统学习,我推荐这个教程:www.liaoxuefeng.com/wiki/101695…


此时,这个虚拟环境就创建好了,我们可以看到根目录下多了一个橙红色的venv文件夹。

golang会把所有的包都下载到同一个目录,然后使用包名+版本唯一指定一个包,并在编译时读取go.mod,获取对应的包+版本。而python不同,python的包的目录下,一个包只会有一个文件夹,不会有多版本共存的情况。因此虚拟环境就是帮你创建好一个仅有这个项目可用的,下载第三方包的文件夹。

然后我们再创建一个requirements.txt文件,一个pytest.ini文件

requirements.txt这个文件类似golang的go.mod文件,用来指定依赖与版本的。不过requirements.txt是一个给人看的文件,python在运行时不会检查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的运行配置文件。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,

AI写代码bash

    最后两步,我们安装一下依赖,并检查一下

    目前根目录下应该有这些东西(如图左)

    打开pycharm自带的命令行工具,并运行pip3 install -r requirements.txt(如图右)


    接着在命令行里输入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"

    AI写代码bash

    在命令行中输入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()}"

    AI写代码bash

    报错信息对比


    生成测试报告

    可以注意到,刚刚运行完之后,在根目录多了一个reports文件夹。这个文件夹存放了测试结果,但是这个测试结果不是人类可读的,因此我们需要生成一个人类可读的测试报告。虽然命令行的输出也能看,但是生成的测试报告实在太方便了,因此我强烈建议用网页版的测试报告来查看测试结果。

    我们首先安装一下allure:brew install allure。然后运行allure generate reports/ecs -o reports/html --clean。我们就生成了人类可读的html测试报告。然后打开这个html(如图左),并把语言转成中文(如图右)


    如果想了解更多关于allure的细节,可以自己摸索与参考这个连接~:qualitysphere.gitee.io/ext/allure/…

    类别、测试套、功能、包,这四个title都可以查看到所有的测试用例执行情况。但是我最推荐**“功能”**来查看,因为这个title展示起来最清晰,需要点击的次数最少。下面是在面对几十个测试用例下,几个tab页的区别。



    查看失败的原因

    回到我们自己的例子上,我们点击功能,点击我们刚刚失败的case(如左图),我们可以看到是哪个case失败了。更进一步,我们点开几个框框,可以看到具体的日志和测试详情。


    Mark

    docs.pytest.org/en/7.1.x/ho…

    打标签

    使用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,会得到如右图的结果:

    可以看到pytest把两个用例都识别出来了,但是只跑了标记了p0的用例,而标记了p1的用例没有跑:

    此外,所有的mark需要提前声明。在这个case中,我们在【先利其器】中创建的pytest.ini文件中已经提前声明好了一些mark(如图左),如果没有提前声明,那么pytest会有报警(如图右):


    暂时不运行

    如果某个用例暂时有问题,或者用例写好了,但是功能没写好,我们可以标记先跳过这个用例(如图左)

    更进一步,我们可以有条件得跳过某些case,比如我们先看看当前的环境,是否在ICU,如果在的话,就不管心跳了。


    同一个用例不同的参数

    比如现在有一个查询的测试接口,我们要测试不同查询条件下接口是否符合预期。从最简单的开始:不同的page size:

    可以看到,虽然我们只写了一个用例,但是利用pytest.mark.parametrize,我们实现了类似表驱动测试的效果。

    下面使用了pytest -k参数。使用细节可以参考FQA-如何只跑一个测试?

    我们也可以在一个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,则只会执行一次。

    更多的例子可以参考这个文档:blog.csdn.net/Tangerine02…

    嵌套

    除了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共同构成唯一一个前置依赖。

      有个小问题,Python如何判断两个值是否相等呢?对于Python而言,判断两个值相等有两个操作符,== 和is。==用来判断值是否相等;is用来判断地址是否相等。对Pytest的fixture而言,其使用is来判断是否相等。因此两个相同的dict也会让fixture执行两遍,只有声明一个全局变量,然后两者都使用这个全局变量,才会让fixture只执行一遍。

      更多关于Python == 和 is的区别,可以参考这个讨论:www.zhihu.com/question/20…

      虽然@pytest.mark.parametrize可以设置scope(作用域),并且可以覆盖原有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)

      有两个点需要注意:

      第一是两种方式存在细微的逻辑差别,当下图中红色框框内的代码段报错时(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

        FAQ

        如何只跑一个测试?

        有三种方式

        一种是直接点击pycharm的这个箭头,就可以。如果点击后有些问题,请参考【先利其器】一章节的内容设置一下pycharm

        一种是直接在命令行中调用pytest -k XXX,XXX是你的测试函数或者测试类。如果你的测试函数名与其他地方的测试函数名重合,则可以加上路径,比如pytest FFF -k XXX。其中FFF是你所要运行的用例的路径,精确到文件夹或者文件都可以。

        一种是不使用-k,直接pytest XXX,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就没有这部分日志了。

        allure报告中的黄颜色的item是什么意思?

        意思是代码发生了panic(raise error)。通常是代码没有做错误处理导致的。

        pytest 多线程插件 pytest-parallel 不能和测试报告插件 allure-pytest兼容的问题

        版本pytest-multithreading-allure-1.0.5,使用:requirement.txt

        解决办法:testerhome.com/topics/3274…

        相关问题:github.com/allure-fram…

        参考文档

        docs.pytest.org/en/7.1.x/in…

        blog.csdn.net/tangerine02…

        blog.csdn.net/totorobig/a…

        qualitysphere.gitee.io/ext/allure/

        总结:

        感谢每一个认真阅读我文章的人!!!

        作为一位过来人也是希望大家少走一些弯路,如果你不想再体验一次学习时找不到资料,没人解答问题,坚持几天便放弃的感受的话,在这里我给大家分享一些自动化测试的学习资源,希望能给你前进的路上带来帮助。

        软件测试面试文档

        我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。

        视频文档获取方式:
        这份文档和视频资料,对于想从事【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!以上均可以分享,点下方小卡片即可自行领取。

        版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
        网站建设 2026/4/16 19:52:06

        第52集科立分板机:科立分板机有哪些型号

        科立分板机拥有多个型号&#xff0c;以下是一些主要的型号及其特点&#xff1a;1、KL-3500 四轴自动曲线分板机&#xff1a;功能特点&#xff1a;具备 MARK 点检查和自动校正功能&#xff0c;支持路径优化与矩阵编程&#xff0c;可快速适配多批次换型需求。采用真空吸附与缓速切…

        作者头像 李华
        网站建设 2026/4/18 12:04:03

        商,我们有从哪本杂志见过什么样的人才能挣大钱啊

        我们知道有哈佛商学院&#xff0c;然后搞文学的如何挣到钱&#xff0c;出版社、编辑的工作&#xff0c;群。看看。经济学&#xff0c;就是研究如何促进流通于人际间、from bussiness to b/p、from country 事业机关单位 to b/p&#xff0c;并且在一定的稳定的商业性质的组织结构…

        作者头像 李华
        网站建设 2026/4/18 4:22:05

        【time-rs】DifferentVariant 错误类型详解(error/different_variant.rs)

        DifferentVariant 错误类型详解 这段Rust代码定义了一个表示"不同变体"错误的类型。让我详细解释每个部分&#xff1a; 主要用途 这个错误类型用于表示枚举类型的转换失败&#xff0c;特别是当尝试从一个枚举变体转换为另一个不兼容的变体时。 代码结构分析 1. 类型定…

        作者头像 李华
        网站建设 2026/4/16 12:01:48

        意大利航空携手ESA部署卫星通信技术提升飞行效率

        为了让乘客享受更智能、更绿色、延误更少的飞行体验&#xff0c;意大利航空公司ITA Airways已部署了来自欧洲航天局(ESA)和卫星运营商Viasat的Iris技术。ITA Airways由意大利经济财政部(59%)和德国汉莎航空集团(41%)共同持股&#xff0c;自称为意大利的标杆航空公司&#xff0c…

        作者头像 李华
        网站建设 2026/4/18 8:46:09

        ServiceNow斥资10亿美元收购Veza 加速智能体权限管理

        ServiceNow宣布将以超过10亿美元的价格收购身份安全平台Veza&#xff0c;这是该公司在2025年进行的又一项专注于AI和数据的重要交易。这项收购协议正值各组织加速部署AI智能体的关键时刻&#xff0c;而企业在管理这些非人类身份的访问权限、许可和输出方面正面临挑战。ServiceN…

        作者头像 李华