Pytest 从入门到起飞

# ==================================
# Author : Mikigo
# Time   : 2021/3/23
# ==================================

一、简介

Pytest 是一个流行的根据 Python 的主动化测试结构,以其灵敏性、可扩展性、易用性俘获了大批 Python 程序员的心。网上关于 pytest 的教程不少,可是都是零散的常识点,简直没有很全面的教程,最初学习它的时分就苦于没有全面的教程而不得不去研读它的官方文档,看完一遍官方文档之后感觉还有些没弄明白,到京东上买了一本《pytest测试实战(Brian.Okken》,如获至宝,应该说是当时仅有的一个解说 Pytest 的书本了,看完之后我有点绝望,写得也太简略了,2天就看完了,还不如看官方文档呢,就这都能出版,我都能写书了,后边又回过头来看 Pytest 官方文档。

大家知道官方文档确实是最全的,能够让你全方位的把握,可是,便是由于太详细了,有些东西关于99.9%的运用者来说,其实没必要把握。经过长期摸索,我将咱们常常运用到的、中心的内容总结成这篇教程,期望能以一种轻松、简略、令人愉悦的办法,将这些内容讲清楚。

也期望经过解说各个模块的常识,潜移默化的让你了解 Pytest 的 Plugin 规划哲学。

二、装置

sudo pip3 install pytest

只需求这样装置一下就行了,够简略易用吧。

三、用例编写

1、函数式

def test_001():
    ...
def test_002():
    ...

这种是函数式的用例写法,便是直接在 py 文件里边界说函数,函数里边写测试用例。

2、类式

class TestMikigo:
    def test_001(self):
        ...
    def test_002(self):
        ...

这种写法是根据类的用例写法,在函数里边写测试用例即可。

两种写法都是能够的,在履行的时分都能被 pytest 识别为用例。

从工程的视点,我更推荐运用根据类的用例写法,由于函数式的写法不能运用类等级的 fixture,由于它没有类。根据类的用例写法给咱们供给了更加灵敏的 fixture 的处理,让你在应对杂乱测试场景的时分,挥洒自如。

四、fixture

fixture 翻译过来是 “夹具”,简略讲便是用例履行前后要做的操作,咱们称为前置(setup)和后置(teardown)操作。用例就像夹心饼干相同被夹在中心。

Pytest 灵敏性很重要的一方面便是体现在它的 fixture,那是适当的灵敏。不只支撑多个等级,各个等级的写法还支撑多样,还能够同享、默许调用、指定调用,许多骚操作,现在不知道不要紧,看完这一章应该能有所收成。

1、等级

总共有四个等级:函数等级(funciton)、类等级(class)、模块等级(module)、会话等级(session)

这部分只需求了解概念,不需求纠结与详细的写法。

  • 函数等级(funciton)

    函数便是用例,函数等级的fixture便是在每个用例履行前后的操作,即: function_setup test_case function_teardown

  • 类等级(class)

    类等级便是在每个用例类履行前后要履行的操作,留意,一个类只履行一次哦。

    • 假如一个类里边只需一个函数,类等级相关于函数等级,便是在函数等级前后履行,即:class_setup function_setup test_case function_teardown class_teardown

      你看其实便是在前面函数等级的基础之上外面加了一层,很好了解是吧。

    • 假如一类里边有多个函数,履行的顺序是:class_setup function_setup test_case_001 function_teardown function_setup test_case_001 function_teardown class_teardown

      你留意看差异,中心有多个用例函数,每个函数外都夹了一个函数等级的fixture,然后类等级fixture是放在最外层的。

  • 模块等级(module)

    了解了前面类等级的fixture之后,信任你现已能推理出来了,模块其实能够了解成一个 py 文件,在一个 py 文件里边能够写多个类,每个类里边能够写多个函数。一层夹一层的。

  • 会话等级(session)

    这个相对来讲有点抽象,其实便是你履行一次用例便是一次会话,当然一次会话里边或许只需一个用例,也或许包括多个用例模块、多个用例类、多个用例。

2、不同的写法

这儿骚操作就比较多了,看准了哈。

2.1、XUnit 的写法

了解 unittest 结构的人都知道,unittest 里边 fixture 的写法是 setUptearDownsetUp_classtearDown_class,只需这一种写法,而且是固定的写法哈。

Pytest 是兼容 unittest 的,当然也支撑这样写:

from datetime import datetime
from time import sleep
from unittest import TestCase
class TestMikigo(TestCase):
    def setUp(self):
        print("我是前置", datetime.now())
        sleep(1)
    def test_001(self):
        print("我是用例", datetime.now())
        sleep(1)
    def tearDown(self):
        print("我是后置", datetime.now())

留意是承继了 unittest.TestCase 的,Pytest 能够兼容运转的,根本操作哈。

除此之外还有一些你没见过的 Xunit 写法:

from datetime import datetime
from time import sleep
class TestMikigo:
    def setup(self):
        print("我是前置", datetime.now())
        sleep(1)
    def test_001(self):
        print("我是用例", datetime.now())
        sleep(1)
    def teardown(self):
        print("我是后置", datetime.now())
我是前置 2022-01-24 16:17:59.267900
我是用例 2022-01-24 16:18:00.269282
我是后置 2022-01-24 16:18:01.270688

你看,setupteardown 这种写法也是会被主动识别的。

还有这种,看准了哈:

from datetime import datetime
from time import sleep
class TestMikigo:
    def setup_method(self):
        print("我是前置", datetime.now())
        sleep(1)
    def test_001(self):
        print("我是用例", datetime.now())
        sleep(1)
    def teardown_method(self):
        print("我是后置", datetime.now())
我是前置 2022-01-24 16:24:57.456212
我是用例 2022-01-24 16:24:58.457273
我是后置 2022-01-24 16:24:59.458097

setup_methodteardown_method 也能够。

这儿还要讲一下 setup_methodsetup_function 的差异,setup_function 是在根据函数的用例写法时运用的,setup_method 是根据类的用例写法运用的,我面试他人的时分喜爱问这两个的差异,假如知道这个的,根本对本部分内容是了解的。

以上仅仅用例等级的,下面说下类等级的写法:

from datetime import datetime
from time import sleep
class TestMikigo:
    def setup_class(self):
        print("我是类前置", datetime.now())
        sleep(1)
    def setup_method(self):
        print("我是用例前置", datetime.now())
        sleep(1)
    def test_001(self):
        print("我是用例", datetime.now())
        sleep(1)
    def teardown_method(self):
        print("我是用例后置", datetime.now())
        sleep(1)
    def teardown_class(self):
        print("我是类后置", datetime.now())
我是类前置 2022-01-24 16:31:59.411548
我是用例前置 2022-01-24 16:32:00.411892
我是用例 2022-01-24 16:32:01.413373
我是用例后置 2022-01-24 16:32:02.414377
我是类后置 2022-01-24 16:32:03.415521

直接写成 setup_class 就能够了,在 unitest 里边类等级的是需求加类办法装修器 @classmethod 的,pytest 里边可加可不加,看你喜爱。

模块等级的就用 setup_module,会话等级的就用 setup_session,这都好了解,这儿就不举例了。

2.2、fixture 写法

前面 Xunit 的写法现已很灵敏了,可是 Pytest 真实厉害的是它自己特有的 fixture 写法。

from datetime import datetime
from time import sleep
import pytest
class TestMikigo:
    @pytest.fixture(scope="function")
    def do_something_before(self):
        print("我是用例前置", datetime.now())
        sleep(1)
        yield
        print("我是用例后置", datetime.now())
    def test_001(self, do_something_before):
        print("我是用例", datetime.now())
        sleep(1)

这儿有几点要留意:

  • fixture 必需求加@pytest.fixture() 装修器;

  • scope 为 fixture 等级;

  • fixture 的函数名 do_something_before 能够自界说,可是不要和 Xunit 的函数名相同,否则就乱掉了。

  • yield 之前是前置,yield 之后是后置,这儿实践上是利用了生成器的原理;

  • 函数名 do_something_before 需求显式的传入用例参数:

    def test_001(self, do_something_before):
        print("我是用例", datetime.now())
        sleep(1)
    

    或者运用 @pytest.mark.usefixtures() ,就像这样:

    @pytest.mark.usefixtures("do_something_before")  # 也能够放到类名前面,表明对这个类里边一切的用例都收效。
    def test_001(self):
        print("我是用例", datetime.now())
        sleep(1)
    

    不过这种写法我个人不建议哈,字符串的方式属于硬编码,不好保护。

这儿要好好了解一下哈,短短的几句话,其实包括了许多内容哦。这种写法没有一个严格的定式,更多是一种方式。

3、同享

fixture 同享是 Pytest 的一大特点,也是它灵敏性的重要体现。

3.1、用例之间同享

咱们常常遇到多个用例需求用到同一个 fixture :

from datetime import datetime
from time import sleep
import pytest
class TestMikigo:
    @pytest.fixture(scope="function")
    def do_something_before(self):
        print("我是用例前置", datetime.now())
        sleep(1)
        yield
        print("我是用例后置", datetime.now())
    def test_001(self, do_something_before):
        print("我是用例001", datetime.now())
        sleep(1)
    def test_002(self, do_something_before):
        print("我是用例002", datetime.now())
        sleep(1)
我是用例前置 2022-01-24 18:36:52.039974
我是用例001 2022-01-24 18:36:53.041396
我是用例后置 2022-01-24 18:36:54.042808
我是用例前置 2022-01-24 18:36:54.043354
我是用例002 2022-01-24 18:36:55.044742
我是用例后置 2022-01-24 18:36:56.046134

你看,咱们只界说了一个 fixture,然后将它的函数名 do_something_before 分别都传给了两个用例,这样它就对两个用例都收效了。

那有同学要问了,我假如有许多的用例,每个用例都要传入参数,好麻烦嘞,有没有更妙的写法?

当然有:

from datetime import datetime
from time import sleep
import pytest
class TestMikigo:
    @pytest.fixture(scope="function",autouse=True)
    def do_something_before(self):
        print("我是用例前置", datetime.now())
        sleep(1)
        yield
        print("我是用例后置", datetime.now())
    def test_001(self):
        print("我是用例001", datetime.now())
        sleep(1)
    def test_002(self):
        print("我是用例002", datetime.now())
        sleep(1)

@pytest.fixture 里边有个参数 autouse ,便是主动运用,默许是 False,咱们传 True 就表明对当时效果域下的一切用例都收效。这个 “效果域” 要看这个 fixture 做处的方位,比方例子中,fixture 是在类里边,它就对这个类中的一切用例都收效,假如你把它放到模块里边,它对这个模块里的一切用例都收效。

autouse 一定要慎用,处理不好的话会引起履行逻辑的混乱,便是你会发现有些用例莫名美妙,不知道在履行什么,或许便是有其他没留意到的 fixture 收效了。

3.2、用例类之间同享

前面的例子都是将 fixture 放在类里边,这样只能对这一个类里边的用例收效,要完成用例类之间同享,就不能写在某一个类的里边了:

from datetime import datetime
from time import sleep
import pytest
@pytest.fixture(scope="function", autouse=True)
def do_something_before():
    print("我是用例前置", datetime.now())
    sleep(1)
    yield
    print("我是用例后置", datetime.now())
class TestMikigo001:
    def test_TestMikigo001_001(self):
        print("我是用例TestMikigo002001", datetime.now())
        sleep(1)
class TestMikigo002:
    def test_TestMikigo001_001(self):
        print("我是TestMikigo002用例001", datetime.now())
        sleep(1)

咱们将 fixture 写在module 里边,这样就对文件里边的一切用例都收效。

3.3、超级同享

conftest.py 适当所以 Pytest 的一个本地插件库,你能够在用例的目录结构中任意方位新建一个 conftest.py 文件,然后在里边写入 fixture,这些 fixture 能够对这个 conftest.py 文件对当时目录及子目录下的一切用例收效,所以我称之为“超级同享”,而且各个目录都能够有自己的 conftest.py。

“超级同享” 不是官方术语,是我自创的哈。

咱们来新建一个 conftest.py 文件:

.
├── conftest.py
└── test_case_001.py

在 conftest.py 文件中写一个 fixture:

import pytest
@pytest.fixture(scope="function", autouse=True)
def do_something():
    print("我是用例前置", datetime.now())
    sleep(1)
    yield
    print("我是用例后置", datetime.now())

用例:

class TestMikigo:
    def test_case_001(self):
        print("test case 001")

在根目录运用 pytest -s -v 履行一下:

我是最外层用例前置 2022-02-08 16:20:39.605921
test case 001
我是最外层用例后置 2022-02-08 16:20:40.606656

这样当时目录下哪怕有多个用例文件,里边有成千上万条用例,履行时都会加载这条fixture,你说是不是超级同享。

4、顺序

前面讲“超级同享”说到,咱们能够把 fixture 写在 conftest.py 文件里边,conftest.py 文件对当时目录及子目录下的一切用例收效,而各个目录都能够有自己的 conftest.py,用例文件里边还能够写 fixture,那写了这么多 fixture,它的履行顺序是怎样的,怎样去解除一些效果,这个问题十分重要,理不清楚的话,在实践项目中你会发现这条用例都在干嘛,它为什么履行这个。

跟上思路哈。

首先,从层级上来讲仍然是:会话等级(session)—> 模块等级(module)—> 类等级(class)—> 函数等级(funciton)这样的履行顺序。

然后,相同层级的状况下,要看 fixture 的方位,简略讲便是:外层 fixture 先履行。

咱们以 function 等级的 fixture 举例,结构一个多层级的 fixture,建议你在本地按照下面的描述自己建一个demo,否则或许需求你有比较强的结构化思维:

.
├── cases_1
│ ├── cases_1_1
│ │ ├── conftest.py   # cases_1_1 目录下 conftest
│ │ └── test_cases_1_1.py  # 用例
│ └── conftest.py  # cases_1 目录下 conftest
└── conftest.py  # 最外层 conftest

最外层 conftest:

from datetime import datetime
from time import sleep
import pytest
@pytest.fixture(scope="function", autouse=True)
def do_something():
    print("我是最外层用例前置", datetime.now())
    sleep(1)
    yield
    print("我是最外层用例后置", datetime.now())

cases_1 目录下 conftest:

from datetime import datetime
from time import sleep
import pytest
@pytest.fixture(scope="function", autouse=True)
def do_something_1():
    print("我是 cases_1 层用例前置", datetime.now())
    sleep(1)
    yield
    print("我是 cases_1 层用例后置", datetime.now())

cases_1_1 目录下 conftest:

from datetime import datetime
from time import sleep
import pytest
@pytest.fixture(scope="function", autouse=True)
def do_something_1_1():
    print("我是 cases_1_1 层用例前置", datetime.now())
    sleep(1)
    yield
    print("我是 cases_1_1 层用例后置", datetime.now())

用例里边:

from datetime import datetime
from time import sleep
import pytest
@pytest.fixture(scope="function", autouse=True)
def do_something_out():
    print("我是用例类外面前置", datetime.now())
    sleep(1)
    yield
    print("我是用例类里外面后置", datetime.now())
class TestMikigo:
    @pytest.fixture(scope="function", autouse=True)
    def do_something_in(self):
        print("我是用例类里边前置", datetime.now())
        sleep(1)
        yield
        print("我是用例类里边后置", datetime.now())
    def test_cases_1_1(self):
        print("我是测试用例 1_1")

这样咱们就在不同层级写了一些 function 等级的 fixture,履行一下就能够清楚的看到。

我是最外层用例前置 2022-02-08 15:38:52.924602
我是 cases_1 层用例前置 2022-02-08 15:38:53.925916
我是 cases_1_1 层用例前置 2022-02-08 15:38:54.927332
我是用例类外面前置 2022-02-08 15:38:55.928774
我是用例类里边前置 2022-02-08 15:38:56.930258
我是测试用例 1_1
我是用例类里边后置 2022-02-08 15:38:57.933010
我是用例类里外面后置 2022-02-08 15:38:57.933135
我是 cases_1_1 层用例后置 2022-02-08 15:38:57.933211
我是 cases_1 层用例后置 2022-02-08 15:38:57.933277
我是最外层用例后置 2022-02-08 15:38:57.933338

从上到下,你仔细看之后,定论呼之欲出:外层先履行

外层 conftest 先于内层 conftest,内层 conftest 先于类外面的,类外面的先于类里边的。

所以咱们在写fixture的时分一定要特别留意,Pytest 给咱们供给的很灵敏很便利的fixture的各种写法,特别是加了 autouse=True 之后,假如你搞不清楚fixture哪个先履行哪个后履行,那你最好就别用这些写法,老老实实用 Xunit 的写法,现已能满足多大部分项目需求了。

留意,以上咱们写的这些fixture的函数名都是不同的,假如是相同的函数名会呈现什么状况呢?

咱们把 do_something_out 改成 do_something_in ,两个 fixture 函数名都是 do_something_in,咱们看看会产生什么。

我是最外层用例前置 2022-02-08 15:56:03.138015
我是 cases_1 层用例前置 2022-02-08 15:56:04.138356
我是 cases_1_1 层用例前置 2022-02-08 15:56:05.139590
我是用例类里边前置 2022-02-08 15:56:06.140796
我是测试用例 1_1
我是用例类里边后置 2022-02-08 15:56:07.142623
我是 cases_1_1 层用例后置 2022-02-08 15:56:07.142667
我是 cases_1 层用例后置 2022-02-08 15:56:07.142694
我是最外层用例后置 2022-02-08 15:56:07.142715

你看,类外面的 do_something_in 没有履行,阐明相同函数名的 fixture,只会履行内层的。

这点常识也十分重要哈,由于在实践项目中,咱们常常会遇到有些用例我不想用某些外层的 fixture,所以咱们能够在内层界说一个同名的 fixture,里边写pass,适当于抵消掉了外层 fixture 的效果。

五、断语

1、惯例断语

Pytest 本身并没有供给断语的办法,而是直接运用Python自带的 assert 句子进行断语。

def test_case_001():
    assert 1 == 1

assert 后边直接写表达式就好了,so easy!

2、自界说断语

运用 assert 断语很简略,可是抛的反常日志根本没什么参阅含义,由于在断语失利的时分,只会提示你 assert 后边的表达式不成立,这不是废话吗,肯定不成立才断语失利噻,问题是我的表达式或许是一些比较杂乱的封装,这儿边详细什么问题就不知道了。因此,运用自界说断语会比较好。

从本质上讲,断语失利实践上都是捕获的 AssertionError(断语反常),所以咱们只需求界说一个自界说反常类,然后抛一个 AssertionError 就能够了。

class AssertCommon:
    @staticmethod
    def assert_true(expect):
        """断语为真"""
        if not expect:
            raise AssertionError(f"<{expect}>不为真")

经过给 AssertionError 传入自界说的字符串,用例失利时,咱们就能够看到明确的失利信息。

六、命令行参数

运用 Pytest 履行用例时,咱们常常都是经过命令行来履行的,有同学要说了,我一般是经过编辑器里边直接就履行了;在实践项目中编写用例调试用例,运用编辑器履行用例没问题,但在 CI 集成环境下,一般是需求用命令行的。

Pytest 要想玩得溜,命令行参数必需求了解,Pytest 支撑的参数许多,有自带的参数,插件供给的参数,还有咱们自己界说的一些参数,下面就介绍在项目中常用的参数:

1、-s

有时分你发现在用例里边运用 print 句子,可是履行的时分却没有打印,那八成是由于你没有加这个参数。等价于 --capture=no,用处便是捕获 print 输出。假如你不知道 capture 参数也不要紧,不重要。

2、-v

详细展示终端输出。比详细更详细运用 -vv ,咱们当然是期望输出信息越详细越好。

3、-k

履行用例的时分十分有用,经过关键词来匹配用例,用例的关键词有许多,模块名、文件名、类名、函数名都是关键词,比方:

pytest -k "test_music"

表明履行一切包括 test_music 关键词的用例。

-k 还有一点或许许多同学都不知道,它还支撑逻辑表达式,比方:

pytest -k "test_music or test_movie"
pytest -k "not test_music and not test_movie""

逻辑表达是支撑 and/or/not 的逻辑组合。

在批量支撑用例时,咱们通常是不需求履行全量用例的,学会精准的组装用例集关于主动化测试十分重要。

4、-m

咱们能够给用例打上标签(mark):

import pytest
class TestMikigo:
    @pytest.mark.smoke
    def test_case_001(self):
        ...

运用装修器 @pytest.mark 点后边加标签名,就能够给用例打标签,标签名随意指定,甚至能够用中文。

打完标签之后,批量履行用例时就能够经过标签来加载用例,用法和 -k 是相同的。

pytest -m "smoke or core"

5、–co

这个参数全称是 --collect-only,表明只搜集用例,不履行。

每周我需求给老板陈述现在一切用例多少条,咱总不或许在代码里边一条条去数吧,我通常会运用:

pytest --co

直接就能够看到加载了多少条用例。

另外,有时分批量修正了一些代码,或许引起一些错误,咱们能够经过履行 pytest --co 来快速检测一下是否存在错误,由于 Pytest 在加载用例的时分同时也会检测代码中存在的一些问题。这个也十分好用。

6、maxfail

装备最大失利次数,假如一次履行呈现了大量的失利,八成这次测试是无效的,经过装备这个参数,咱们不必比及一切用例履行完才完毕,尽早完毕节省时刻。

pytest --maxfail=int_number

这儿的 int_number 便是最大的失利次数,你能够根据你的经历来指定一个数字。

我的计划是先获取到本非必须履行的总用例数 collected_cases_num,然后装备一个总数的比例,如 0.5,表明只需失利次数达到了总数的一半,就能够直接完毕测试。

pytest --maxfail=int(collected_cases_num * 0.5)

这样做的优点是,随着项目中用例数量的增加,我不需求去修正这个最大失利的数据,而是经过装备整体的失利比例,这样做更加合理,也更易于保护。

7、reruns

失利重跑次数,在主动化测试进程中常常会有一些不确定性,网络问题、环境问题、量子力学、地球引力等等都有或许造成用例失利,特别是 UI 主动化测试,这些状况常常产生,为了尽量扫除环境问题造成的用例失利,采用失利后主动重跑是一个比较好的计划。

pytest --reruns=2

表明失利后重跑2次,假如后边重跑用例成功了,终究的用例状态为 PASSED。

这个参数需求装置三方插件:

sudo pip3 install pytest-rerunfailures

8、timeout

用例超时在 CI 流程中十分重要,由于一切的每日构建都应该是有时长约束的,一跑便是两三天不断就不叫每日构建了,用例履行进程中或许存在一些反常状况,导致用例卡住不动,或者履行速度变慢,咱们运用 --timeout 能够给每条用例设置一个最大的时长,假如超时没有履行完,便是强制停止用例。

pytest --timeout=200

表明每条用例的超时时刻为 200 秒,留意单位是秒哦。

这个参数需求装置三方插件:

sudo pip3 install pytest-timeout

9、自界说命令行参数

以上参数都是 Pytest 自带的或者三方插件给咱们供给的参数,当这些参数不能满足咱们的需求的时分咱们就需求自界说一些命令行参数。

首先,咱们需求注册命令行参数,前面讲超级同享的时分讲了 conftest.py,可是 conftest.py 能做的事情可不只仅是写点 fixture,它可是 Pytest 的本地插件,在里边咱们能够写 hook(钩子)函数,这儿咱们介绍其间一个 hook 函数,即完成自界说命令行参数的 hook 函数,其他的 hook 函数咱们后边会介绍到。

官方说法是:注册命令行选项。实践便是自界说命令行参数。

举例:

def pytest_addoption(parser):
    parser.addoption()

parser.addoption() 里边能够传入挺多参数的,可是不是一切的都需求:

1、name:自界说命令行参数的名字,能够是:"foo", "-foo" 或 "--foo";
2、action:在命令行中遇到此参数时要采取的根本操作类型;
3、nargs:应该运用的命令行参数的数量;
4、const:某些操作和nargs挑选所需的常量值;
5、default:假如参数不在命令行中,则生成的默许值。
6、type:命令行参数应该转换为的类型;
7、choices:参数允许值的容器;
8、required:命令行选项是否能够省掉(仅可选);
9、help:对参数效果的扼要阐明;
10、metavar:用法消息中参数的称号;
11、dest:要增加到 parse_args() 返回的目标中的特点的称号;

常用的几个参数就这几个,以下是项目实例:

def pytest_addoption(parser):
    parser.addoption(
        "--logLevel",
        action="store",
        default="DEBUG",
        help="DEBUG, INFO,WARNING,ERROR, CRITICAL, 终端日志输出等级",
    )

用于控制日志输出的等级,CI 集成环境下咱们不需求输出 DEBUG 等级的日志,咱们能够这样用:

pytest --logLevel=INFO

这样在履行用例的时分,参数就能够传递进来,那么传递进来之后,咱们在哪里用?

同样是在 conftest.py 里边:

(1)经过 fixture 里边的 request 目标:

@pytest.fixture(scope="session")
def do_something(request):
    logLevel = request.config.getoption("--logLevel")

这样能够获取到,request.config.getoption 是固定写法,常常有同学问你咋知道能够这么用呢,在哪里能够看到,实践上能够经过给 request 打断点,你会看到这个目标内有哪些办法。

(2)经过 hook 函数里边的 seesion 目标:

def pytest_sessionstart(session):
    logLevel = session.config.option.logLevel

这儿的 hook 函数纷歧定是 pytest_sessionstart,许多 hook 函数都能够。

(3)经过 hook 函数里边的 item 目标:

def pytest_runtest_teardown(item):
    logLevel = item.session.config.option.logLevel

(4)在用例中运用:

def test_xxx(pytestconfig):
    log_level = pytestconfig.option.logLevel

pytestconfigpytest 供给的一个内容 fixture ,能够获取到一切的参数;获取参数值的办法除了 pytestconfig.option.logLevel 这种写法,pytestconfig.getoption("logLevel") 这种写法也是 ok 的。

仔细观察,获取命令行参数都在 config 这个目标里边,以上举例的不同的 hook 函数默许的参数是不同的。

七、参数化

参数化是主动化测试里边十分重要的一个特性,特别是关于接口主动化测试,那是肯定要用到的。我看到许多网上的教程将参数化称为高级技术,经过这点能看出来,高级也不咋的哈。

Pytest 的参数化运用办法也很简略,运用装修器:

@pytest.mark.parametrize("par_1", [1, 2])
def test_case_001(par_1):
    ...
  • 装修器是固定用法 @pytest.mark.parametrize,记住就行了,记不住就记住两个东西,首先是在 mark 里边,然后 p 开头,最后用编辑器补全就好了。

  • 参数有 3 个:

    • 第 1 个参数是字符串类型,里边是参数的变量称号,多个变量需求用逗号分隔;

      这也是最常见的用法,网上教程根本都是这样讲的,但假如你看过 Pytest 源代码这个参数类型是这样界说的: argnames: Union[str, List[str], Tuple[str, ...]] 阐明列表和元组也能够,只需里边是字符串就行。

    • 第 2 个参数是一个列表,精确的讲是一个可迭代目标,一般咱们就用列表好了,假如是多个参数便是 2 维列表,列表中每个元素的个数对应变量的个数。

      @pytest.mark.parametrize("par_1, par_2", [[1, 2], [3, 4]])
      def test_case_001(par_1, par_2):
          ...
      

      这个参数虽然是可迭代目标,但你最好别运用随机东西生成(每次都是随机数),这或许影响重跑失利用例等一些功用,也最好别运用生成器放里边。

    • 第 3 个参数是 ids,这个参数是符号 ID 的,不传也能够,差异便是不传的话终端输出的用例标题会主动加上参数,或许许多同学没用过,不了解也不要紧哈,本身用的不多。

用例的参数化用法便是这么简略。

Pytest 其实还支撑 fixture 的参数化,这也是 fixture 相关于 Xunit 写法的其间一点优势,但在实践项目中很少用,没有太多这样的需求,这儿就不讲了。

八、Hook(钩子)函数

hook 函数适当所以 Pytest 的一些本地插件,Pytest 给咱们供给了许多的 hook 函数,用于处理不同阶段的自界说行为。

有几个留意点:

  • hook 函数一般不建议写在非根目录下的 conftest 插件文件里边,咱们一般是写在最外层的那个 conftest 里边。
  • hook 函数都是以 pytest_ 开头的函数。
  • 不同的 hook 函数有它自己的功用和所属的阶段。

1、hook 函数略览

Pytest 内置了许多的 hook 函数供咱们运用,下面咱们就按照阶段划分,罗列一下有哪些 hook 函数,能够大致感受一下:

引导钩子:

pytest_load_initial_conftests
pytest_cmdline_preparse
pytest_cmdline_parse
pytest_cmdline_main

初始化钩子:

pytest_addoption
pytest_addhooks
pytest_configure
pytest_unconfigure
pytest_sessionstart
pytest_sessionfinish
pytest_plugin_registered

搜集钩子:

pytest_collection
pytest_collect_directory
pytest_collect_file
pytest_pycollect_makemodule
pytest_pycollect_makeitem
pytest_generate_tests
pytest_make_parametrize_id
pytest_collection_modifyitems
pytest_collection_finish

测试运转(runtest)钩子:

pytest_runtestloop
pytest_runtest_protocol
pytest_runtest_logstart
pytest_runtest_logfinish
pytest_runtest_setup
pytest_runtest_call
pytest_runtest_teardown
pytest_runtest_makereport
pytest_pyfunc_call

陈述钩子:

pytest_collectstart
pytest_make_collect_report
pytest_itemcollected
pytest_collectreport
pytest_deselected
pytest_report_header
pytest_report_collectionfinish
pytest_report_teststatus 
pytest_terminal_summary
pytest_fixture_setup
pytest_fixture_post_finalizer
pytest_warning_recordedLiteral 
pytest_runtest_logreport
pytest_assertrepr_compare
pytest_assertion_pass

调试钩子:

pytest_internalerror
pytest_keyboard_interrupt
pytest_exception_interact
pytest_enter_pdb

这些钩子函数都是 Pytest 给咱们供给的,你能够在 conftest 插件文件里边去重写函数来完成你的自界说功用。

看到这么多函数先别慌,咱不需求把握一切的,由于许多是不常用的,也便是我开篇说到的 99% 的人都用不到的,其间常常运用到的一些比较常用的 hook 函数,将会在后续内容介绍到。

2、常用的 hook 函数

2.1、pytest_addoption

这个 hook 函数在前面将自界说命令行参数的时分现已用过了,它的用处便是注册命令行参数,这些值在测试运转开始时会被调用一次。

# conftest.py
def pytest_addoption(parser):
    parser.addoption()

前面有例子,这儿就不多讲了,用法很简略。

它都是经过 parser.addoption 在界说命令行参数的。

2.2、pytest_configure

这个函数主要是用来获取命令行参数的:

# conftest.py
def pytest_configure(config):
    my_option = config.getoption("--opt")

是在履行测试履行运转,简略哈。

pytest_unconfigure 则是在履行测试退出之前运转。

2.3、pytest_sessionstart

这个函数在 session 目标创立之后,履行搜集之前调用:

def pytest_sessionstart(session):
    """
    :param pytest.Session session: The pytest session object.
    """

session 目标里边有许多特点,常用的:

  • startdir:用例根目录的绝对途径。
  • items:用例目标的列表。
  • config:config目标。

你也能够往里边动态增加一些特点。

和它对应的 pytest_sessionfinish 是在一切测试完毕,退出之前履行。

2.4、pytest_collection_modifyitems

这个函数主要用来调整用例:

# conftest.py
def pytest_collection_modifyitems(session, config, items):
    """
    :param pytest.Session session: The pytest session object.
    :param _pytest.config.Config config: The pytest config object.
    :param List[pytest.Item] items: List of item objects.
    """
  • session 为 Pytest 的 session 目标。

  • config 为 Pytest 的 config 目标。

  • items 是一个列表,其他每个元素便是一个用例目标。

    item 里边有许多特点,常用的:

    name:用例的称号
    nodeid:从用例根目录开始到用例文件的途径
    own_markers:用例的mark标签
    

pytest_collection_finish 则是在搜集完而且修正完之后运转,它是在 pytest_collection_modifyitems 之后的。

2.5、pytest_runtest_setup

这个函数是在调用 setup 的时分运转:

def pytest_runtest_setup(item):
    ...

留意是 item,不是 items,item 是用例目标。

比方,你能够在每次用例履行之前输出用例的标题:

def pytest_runtest_setup(item):
    logger.info(item.function.__name__)

pytest_runtest_callpytest_runtest_teardown 分别是在用例履行进程中和用例 teardown 阶段运转,用法是相同的。

2.6、pytest_runtest_makereport

这个函数是用于创立测试陈述的,每个测试用例的测试陈述都分为 setup、call 和 teardown 三个测试阶段。假如你了解 allure 陈述的话,应该能轻易 get 到我说的。

def pytest_runtest_makereport(item):
    out = yield
    report = out.get_result()

九、测试陈述

1、allure

Pytest 最常用的测试陈述是 allure,它是一个三方插件,装置它:

sudo pip3 install allure-pytest

运用也很简略,只需求履行时加参数 --alluredir=xxx 即可:

pytest --alluredir=report

这样就能在 report 目录下生成陈述,翻开它你会发现 report 目录下是一堆文本文件,这咋看呢?

还没完~

你还需求装置 allure 的检查东西,你能够直接从 github上去下载最新的版本,然后装置它即可,比方我现在下来装置是这样的:

sudo dpkg -i allure_2.19.0-1_all.deb

检查陈述:

  • 在线检查:
allure serve report
  • 生成本地 html
allure generate report -o allure-report

这样在 allure-report 目录下会一堆文件,其间有一个 index.html 是陈述的主文件。

运用浏览器直接翻开它,你会发现没有数据!

还没完~~

运用命令翻开:

allure open allure-report

不出意外浏览器会主动翻开陈述。

需求阐明的是,allure 可不是专门为 Pytest 开发的,经过很长时刻的开展,它现已进化为一个陈述结构了,能支撑十分多的语言及东西生成测试陈述,总归这玩意儿老狠了。想深化研究的同学看这儿 allure 。

2、pytest-html

上古时期 Pytest 的“糟糠之妻”,虽然该项目是由 Pytest 官方在保护,可是社区热度不行了,主要是由于颜值不行,很少有人用,也不想过多的介绍它,走个过场吧。

装置:

sudo pip3 install pytest-html

运用:

pytest --html=report.html

3、xml

这是个 Pytest 自带的陈述,不需求装置插件:

pytest --junit-xml=report.xml

我个人仍是十分喜爱这种陈述方式的,由于它不需求装置其他插件,而且处理陈述中的数据也十分简略。

十、Pytest 结构中心

咱们通常根据 Pytest 来编写搭建咱们自己的测试结构,Pytest 天然就成了咱们自己测试结构的中心,那么同学们有没有想过,Pytest 的中心是什么?

信任 90% 的同学没去考虑过或者没了解过~

Pluggy 便是 Pytest 的中心结构,简直 Pytest 一切的功用都是根据 Pluggy 完成的,这是一个被极少人知道且严重轻视的结构,它究竟是什么玩意儿,咱们这儿简略聊一聊。

Pluggy 是从 Pytest 结构中被开发者抽取出来的,由于开发者发现他们规划的这种插件化计划,完全可用在其他的项目中,因此他们总结提炼出了 Pluggy 项目;

令人直呼“厚礼谢”的是整个 Pluggy 项目加起来也就 1100 来行代码(包括注释 ),这便是 Pytest 的中心基石,就像是 Linux 体系中的 Linux 内核的效果。了解它你才能真实了解 Pytest 插件化规划哲学。

为什么说它被轻视了,github 上才不到 900 的 star,真的是深藏功与名!

咱们先随意写个函数:

class Mikigo:
    def miki(self, arg1, arg2):
        """这是一个能够定制的钩子"""

通常,类里边的函数咱们想要重写,只需求承继这个类,然后重写掩盖函数就好了,这便是类的多态,不多讲了;

那咱们运用 Pluggy 怎样去掩盖类里边的函数呢?

import pluggy
# hook 标准,用于生成一个润饰器去符号函数作为钩子函数
hookspec = pluggy.HookspecMarker("mikigo") 
# hook 完成,用于生成一个润饰器去符号函数作为钩子函数的详细完成
hookimpl = pluggy.HookimplMarker("mikigo")
class Mikigo:
    @hookspec
    def miki(self, arg1, arg2):
       """这是一个能够定制的钩子"""
class Plugin_1:
    @hookimpl
    def miki(self, arg1, arg2):
        print("我是 Plugin_1.miki()")
        return arg1 + arg2
# 实例化一个插件管理器目标    
pm = pluggy.PluginManager("mikigo")
# 将 Mikigo 类增加进去
pm.add_hookspecs(Mikigo)
# 注册插件类
pm.register(Plugin_1())
# 经过 hook 特点拜访函数
results = pm.hook.miki(arg1=1, arg2=2)
print(results)
我是 Plugin_1.miki()
[3]

你看,原来的 Mikigo 类里边的函数是没有履行的,而是履行了 Plugin_1 类里边的函数,而且获取到了返回值。

  • PluginManager 是整个插件体系的管理器,传递一个工程称号即可进行实例化;

  • add_hookspecs 增加一个钩子到管理器中,参数是 module 或 class 目标,底层实践是经过 dir() 自省函数进行遍历增加到一个 names 的容器中。

  • register 注册插件

假如咱们还想增加一个插件呢?

import pluggy
# hook 标准,用于生成一个装修器去符号函数作为钩子函数
hookspec = pluggy.HookspecMarker("mikigo") 
# hook 完成,用于生成一个装修器去符号函数作为钩子函数的详细完成
hookimpl = pluggy.HookimplMarker("mikigo")
class Mikigo:
    @hookspec
    def miki(self, arg1, arg2):
       "这是一个能够定制的钩子"
class Plugin_1:
    @hookimpl
    def miki(self, arg1, arg2):
        print("我是 Plugin_1.miki()")
        return arg1 + arg2
class Plugin_2:
    @hookimpl
    def miki(self, arg1, arg2):
        print("我是 Plugin_2.miki()")
        return arg1 - arg2
# 实例化一个插件管理器目标    
pm = pluggy.PluginManager("mikigo")
# 将 Mikigo 类增加进去
pm.add_hookspecs(Mikigo)
# 注册插件类
pm.register(Plugin_1())
pm.register(Plugin_2())
# 经过 hook 特点拜访函数
results = pm.hook.miki(arg1=1, arg2=2)
print(results)
我是 Plugin_2.miki()
我是 Plugin_1.miki()
[-1, 3]

Plugin_2 先履行,然后 Plugin_1 也履行了,两个插件的返回值也都放到一个列表里边了,是不是很神奇。

那假如想 Plugin_1 先履行,Plugin_2 后履行呢?

import pluggy
# hook 标准,用于生成一个润饰器去符号函数作为钩子函数
hookspec = pluggy.HookspecMarker("mikigo")
# hook 完成,用于生成一个润饰器去符号函数作为钩子函数的详细完成
hookimpl = pluggy.HookimplMarker("mikigo")
class Mikigo:
    @hookspec
    def miki(self, arg1, arg2):
        "这是一个能够定制的钩子"
class Plugin_1:
    @hookimpl(tryfirst=True)
    def miki(self, arg1, arg2):
        print("我是 Plugin_1.miki()")
        return arg1 + arg2
class Plugin_2:
    @hookimpl(trylast=True)
    def miki(self, arg1, arg2):
        print("我是 Plugin_2.miki()")
        return arg1 - arg2
# 实例化一个插件管理器目标
pm = pluggy.PluginManager("mikigo")
# 将 Mikigo 类增加进去
pm.add_hookspecs(Mikigo)
# 注册插件类
pm.register(Plugin_1())
pm.register(Plugin_2())
# 经过 hook 特点拜访函数
results = pm.hook.miki(arg1=1, arg2=2)
print(results)
我是 Plugin_1.miki()
我是 Plugin_2.miki()
[3, -1]

能够经过 tryfirsttrylast 来控制,有同学应该有印象, Pytest hook 里边也是用 tryfirsttrylast 控制其 hook 函数履行的先后顺序的,其底层完成是从这儿来的。

正是由于能够随意增加插件以及修正插件的履行顺序,才使得在 Pytest 中咱们能灵敏的界说一些 hook 函数来完成咱们想要的功用,Pytest 以及其三方插件,都是根据此根本逻辑构建起来的。

这儿就简略介绍一下根本原理,想要深化了解的同学点这儿 Pluggy。